diff --git a/.github/nativefuncs.json b/.github/nativefuncs.json index f52a969cb..751b48934 100644 --- a/.github/nativefuncs.json +++ b/.github/nativefuncs.json @@ -449,76 +449,10 @@ "argTypes":"" }, { - "name":"NSGetServerName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerDescription", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerMap", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerPlaylist", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerPlayerCount", - "helpText":"", - "returnTypeString":"int", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerMaxPlayerCount", - "helpText":"", - "returnTypeString":"int", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerID", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex" - }, - { - "name":"NSServerRequiresPassword", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerRequiredModsCount", - "helpText":"", - "returnTypeString":"int", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerRegion", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex" - }, - { - "name":"NSGetServerRequiredModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex, int modIndex" - }, - { - "name":"NSGetServerRequiredModVersion", - "helpText":"", - "returnTypeString":"string", - "argTypes":"int serverIndex, int modIndex" + "name": "NSGetGameServers", + "helpText": "Gets all fetched servers", + "returnTypeString": "array", + "argTypes": "" }, { "name":"NSClearRecievedServerList", diff --git a/Northstar.Client/mod.json b/Northstar.Client/mod.json index 00b779a4e..e9536d663 100644 --- a/Northstar.Client/mod.json +++ b/Northstar.Client/mod.json @@ -1,7 +1,7 @@ { "Name": "Northstar.Client", "Description": "Various ui and client changes to fix bugs and add better support for mods", - "Version": "1.13.0", + "Version": "1.14.0", "LoadPriority": 0, "InitScript": "cl_northstar_client_init.nut", "ConVars": [ @@ -115,6 +115,17 @@ "RunOn": "UI" }, { + "Path": "ui/ns_slider.nut", + "RunOn": "UI" + }, + { + "Path": "ui/menu_mod_settings.nut", + "RunOn": "UI", + "UICallback":{ + "Before": "AddModSettingsMenu" + } + }, + { "Path": "ui/ui_mouse_capture.nut", "RunOn": "UI" } diff --git a/Northstar.Client/mod/materials/vgui/reset.vmt b/Northstar.Client/mod/materials/vgui/reset.vmt new file mode 100644 index 000000000..84034586c --- /dev/null +++ b/Northstar.Client/mod/materials/vgui/reset.vmt @@ -0,0 +1,12 @@ +"Basic" +{ + "$basetexture" "vgui/reset" + "$translucent" 1 + "$vertexcolor" 1 + "$vertexalpha" 1 + "$no_fullbright" 1 + "$ignorez" 1 + "$nolod" 1 + + "$SHADERSRGBREAD360" 1 +} diff --git a/Northstar.Client/mod/materials/vgui/reset.vtf b/Northstar.Client/mod/materials/vgui/reset.vtf new file mode 100644 index 000000000..5ffb86a9c Binary files /dev/null and b/Northstar.Client/mod/materials/vgui/reset.vtf differ diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt index c25708a6f..5dabd5398 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt @@ -321,5 +321,26 @@ Press Yes if you agree to this. This choice can be changed in the mods menu at a "INVALID_MASTERSERVER_TOKEN" "Invalid or expired masterserver token" "JSON_PARSE_ERROR" "Error parsing json response" "UNSUPPORTED_VERSION" "The version you are using is no longer supported" + + // Mod Settings + "MOD_SETTINGS" "Mod Settings" + "NORTHSTAR_BASE_SETTINGS" "Northstar Base Settings" + "ONLY_HOST_MATCH_SETTINGS" "Only Host can change Private Match settings" + "ONLY_HOST_CAN_START_MATCH" "Only Host can Start the Match" + "MATCH_COUNTDOWN_LENGTH" "Private Match Countdown Duration" + "LOG_UNKNOWN_CLIENTCOMMANDS" "Log Unknown Client Commands" + "DISALLOWED_TACTICALS" "Prohibited Tacticals" + "TACTICAL_REPLACEMENT" "Replacement Tactical" + "DISALLOWED_WEAPONS" "Prohibited Weapons" + "REPLACEMENT_WEAPON" "Replacement Weapon" + "SHOULD_RETURN_TO_LOBBY" "Return To Lobby After Match End" + "ARE_YOU_SURE" "Are you sure?" + "WILL_RESET_ALL_SETTINGS" "This will reset ALL settings that belong to this category.\n\nThis is not revertable." + "WILL_RESET_SETTING" "This will reset the %s1 setting to it's default value.\n\nThis is not revertable." + "MOD_SETTINGS_SERVER" "Server" + "MOD_SETTINGS_RESET" "Reset" + "MOD_SETTINGS_RESET_ALL" "Reset All" + "NO_RESULTS" "No results." + "NO_MODS" "No settings available! Install more mods at ^5588FF00northstar.thunderstore.io^0." } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_french.txt b/Northstar.Client/mod/resource/northstar_client_localisation_french.txt index 2a1991869..d90cea05f 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_french.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_french.txt @@ -319,5 +319,23 @@ Choisissez Oui si vous êtes d'accord. Ce choix peut être modifié à tout inst "INVALID_MASTERSERVER_TOKEN" "Jeton du server maître invalide ou expiré" "JSON_PARSE_ERROR" "Une erreur est survenue durant l'analyse JSON" "UNSUPPORTED_VERSION" "La version que vous utilisez n'est plus supportée" + + "MOD_SETTINGS" "Paramètres de mod" + "NORTHSTAR_BASE_SETTINGS" "Paramètres de base de Northstar" + "ONLY_HOST_MATCH_SETTINGS" "Seul l'hôte peut changer les paramètres de match privé" + "ONLY_HOST_CAN_START_MATCH" "Seul l'hôte peut lancer le match" + "MATCH_COUNTDOWN_LENGTH" "Durée du compte à rebours de match privé" + "LOG_UNKNOWN_CLIENTCOMMANDS" "Enregistrer les commandes client inconnues" + "DISALLOWED_TACTICALS" "Capacités interdites" + "TACTICAL_REPLACEMENT" "Capacités de remplacement" + "DISALLOWED_WEAPONS" "Armes interdites" + "REPLACEMENT_WEAPON" "Armes de remplacement" + "SHOULD_RETURN_TO_LOBBY" "Retour au lobby après la fin du match" + "ARE_YOU_SURE" "Êtes-vous certain ?" + "WILL_RESET_ALL_SETTINGS" "Ceci réinitialisera tous les paramètres de cette catégorie.\n\nCette action est irréversible." + "WILL_RESET_SETTING" "Ceci réinitialisera le paramètre %s1 à sa valeur par défaut.\n\nCette action est irréversible." + "MOD_SETTINGS_SERVER" "Serveur" + "MOD_SETTINGS_RESET" "Réinitialiser" + "MOD_SETTINGS_RESET_ALL" "Tout réinitialiser" } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_german.txt b/Northstar.Client/mod/resource/northstar_client_localisation_german.txt index 0316bbcf7..f5c814f5a 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_german.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_german.txt @@ -310,5 +310,6 @@ Drücke Ja, um zuzustimmen. Du kannst diese Entscheidung jederzeit im Modmenü "INVALID_MASTERSERVER_TOKEN" "Ungültiger oder abgelaufener Token vom Masterserver" "JSON_PARSE_ERROR" "Fehler beim Verarbeiten der JSON-Antwort" "UNSUPPORTED_VERSION" "Die Version die du benutzt ist nicht länger unterstützt" + "SNS_LEADER_BANKRUPT_SUB" "%s1 Wurde Von %s2 Zurückgesetzt" } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt b/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt index 72bf70309..38e67dea4 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt @@ -7,18 +7,18 @@ "MENU_TITLE_MODS" "Mods" "RELOAD_MODS" "Ricarica Mods" "WARNING", "Attenzione" - "CORE_MOD_DISABLE_WARNING", "Disattivare Mods Principali può rompere il tuo Client!" + "CORE_MOD_DISABLE_WARNING", "Disattivare le Mods Principali può rompere il tuo Client!" "DISABLE", "Disattiva" "DIALOG_TITLE_INSTALLED_NORTHSTAR" "Grazie per aver installato Northstar!" "AUTHENTICATION_AGREEMENT_DIALOG_TEXT" "Affinché Northstar funzioni, è necessario autenticarsi utilizzando il server principale di Northstar. Ciò richiederà l'invio del tuo token di Origin al server principale, non verrà archiviato o utilizzato per altri scopi. -Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi momento nel menu mods." +Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi momento nel Menu delle Mods." "BACK_AUTHENTICATION_AGREEMENT" "Accordo di autenticazione" "AUTHENTICATION_AGREEMENT" "Accordo di autenticazione" "AUTHENTICATION_AGREEMENT_RESTART" "Dovrai riavviare Titanfall 2 affinché questa scelta abbia effetto." "DIALOG_AUTHENTICATING_MASTERSERVER", "Autenticazione Sul Master Server in corso" - "AUTHENTICATIONAGREEMENT_NO", "Hai Scelto di non autenticarti con Northstar. Puoi vedere l'Accordo nel Menu Mods" + "AUTHENTICATIONAGREEMENT_NO", "Hai Scelto di non autenticarti con Northstar. Puoi vedere l'Accordo nel Menu delle Mods" "MENU_TITLE_SERVER_BROWSER" "Server Browser" "NS_SERVERBROWSER_NOSERVERS" "Nessun server trovato" @@ -39,7 +39,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "PRIVATE_MATCH_SINGLEPLAYER_LEVEL" "%s1 (Single-Player)" // fra hint for private match menu, because fra only has PL_fra_desc in vanilla - "PL_fra_hint" "Tutti contro tutti. Uccidi nemici per vincere. Raccogli 3 batterie per chiamare il Titan." + "PL_fra_hint" "Tutti contro tutti. Uccidi i nemici per vincere. Raccogli 3 batterie per chiamare il tuo Titan." // mode settings "MODE_SETTING_CATEGORY_PILOT" "Pilota" @@ -63,7 +63,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "earn_meter_titan_multiplier" "Moltiplicatore nucleo Titan" "aegis_upgrades" "Upgrade Aegis" - "infinite_doomed_state" "Doom state Infinito" + "infinite_doomed_state" "Stato Doom Infinito" "titan_shield_regen" "Rigenerazione Scudi" "riff_floorislava" "Terreno Mortale" @@ -80,12 +80,12 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "cp_amped_capture_points" "Hardpoints Amplificati" "coliseum_loadouts_enabled" "Equipaggiamento Colosseo" - "aitdm_archer_grunts", "Schagniozzi Archer" + "aitdm_archer_grunts", "Scagnozzi Archer" // northstar.custom localisation is just deciding not to work, so putting it here for now "PL_sbox" "Sandbox" - "PL_sbox_lobby" "Sandbox Lobby" - "PL_sbox_desc" "come gmod ma peggio." + "PL_sbox_lobby" "Lobby: Sandbox" + "PL_sbox_desc" "Come gmod ma peggio" "PL_sbox_abbr" "SBOX" "GAMEMODE_SBOX" "Sandbox" @@ -94,9 +94,9 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "PL_gg_desc" "Ottieni un'uccisione con ogni arma per vincere." "PL_gg_hint" "Ottieni una nuova arma ogni uccisione." "PL_gg_abbr" "GG" - "GAMEMODE_GG" "Gioco d'Armi" - "aitdm_archer_grunts", "Schagniozzi Archer" - "gg_kill_reward", "Punteggio Riconpensa Uccisione" + "GAMEMODE_GG" "Gioco delle Armi" + "aitdm_archer_grunts", "Scagnozzi Archer" + "gg_kill_reward", "Punteggio Ricompensa Uccisione" "gg_assist_reward", "Punteggio Ricompensa Assist" "gg_execution_reward", "Punteggio Ricompensa Esecuzione" @@ -126,11 +126,11 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "PL_sns", "Sticks and Stones" "PL_sns_lobby", "Sticks and Stones Lobby" - "PL_sns_desc", "Tutti contro Tutti, Usa la Pulse Blade e Esecuzioni per Resettare il Puntegio Nemico" + "PL_sns_desc", "Tutti contro Tutti, Usa la Pulse Blade e le Esecuzioni per Resettare il Punteggio Nemico" "PL_sns_abbr", "SNS" "GAMEMODE_SNS", "Sticks and Stones" "SCOREBOARD_BANKRUPTS", "Uccisioni Bancarotta" - "SNS_LEADER_BANKRUPT", "Leader Punteggio andato in Bancarotta!" + "SNS_LEADER_BANKRUPT", "Il Leader punteggio è andato in Bancarotta!" "SNS_LEADER_BANKRUPT_SUB", "%s1 è stato Resettato da %s2" "SNS_BANKRUPT", "Bancarotta!" "SNS_BANKRUPT_SUB", "Il Tuo Punteggio è stato Resettato da %s1" @@ -156,12 +156,12 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "INFECTION_YOU_ARE_LAST_SURVIVOR" "Sei l'Ultimo Sopravvissuto!" "INFECTION_SURVIVE_LAST_SURVIVOR" "Sopravvivi." - "PL_tffa", "Titan Free for All" - "PL_tffa_lobby", "Titan Free for All Lobby" + "PL_tffa", "Titan Tutti contro Tutti" + "PL_tffa_lobby", "Lobby: Titan Tutti contro Tutti" "PL_tffa_desc", "Ogni Pilota per sè, distruggi tutti i Titan Nemici." "PL_tffa_hint", "Ogni Pilota per sè, distruggi tutti i Titan Nemici." "PL_tffa_abbr", "TFFA" - "GAMEMODE_TFFA", "Titan Free for All" + "GAMEMODE_TFFA", "Titan Tutti contro Tutti" "PL_hs" "Nascondino" "PL_hs_lobby" "Lobby: Nascondino" @@ -170,22 +170,22 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "PL_hs_abbr" "NS" "GAMEMODE_hs" "Nascondino" "HIDEANDSEEK_YOU_ARE_SEEKER" "TROVA I GIOCATORI NASCOSTI" - "HIDEANDSEEK_SEEKER_DESC" "Trova ed uccidi tutti i giocatori nascosti. \nRespawnerai in %s1 secondi." + "HIDEANDSEEK_SEEKER_DESC" "Trova ed uccidi tutti i giocatori nascosti. \nRespawnerai in %s1 secondi" "HIDEANDSEEK_YOU_ARE_HIDER" "NASCONDITI" "HIDEANDSEEK_HIDER_DESC" "Nasconditi e cerca di non farti trovare." "HIDEANDSEEK_SEEKERS_INCOMING" "HANNO INIZIATO A CERCARTI" "HIDEANDSEEK_DONT_GET_FOUND" "Non farti trovare!" "HIDEANDSEEK_GET_LAST_HIDER" "%s1 è L'ULTIMO CERCATORE" "HIDEANDSEEK_YOU_ARE_LAST_HIDER" "SEI L'ULTIMO NASCOSTO" - "HIDEANDSEEK_GOT_STIM" "Sei stimmato! Non farti prendere!" - "hideandseek_balance_teams" "Bilanciamento Squadre..." + "HIDEANDSEEK_GOT_STIM" "Sei stimolato! Non farti prendere!" + "hideandseek_balance_teams" "Bilanciamento Squadre" "hideandseek_hiding_time" "Tempo per Nascondersi" // these are defined in r1_english but titan war is a shit name so i'm changing it to another one that was referenced in development - "GAMEMODE_fw" "Frontier War" - "PL_fw" "Frontier War" - "PL_fw_lobby" "Lobby: Frontier War" - "PL_fw_desc" "Distruggi il mietiore nemico e proteggi il tuo." + "GAMEMODE_fw" "Guerra di Frontiera" + "PL_fw" "Guerra di Frontiera" + "PL_fw_lobby" "Lobby: Guerra di Frontiera" + "PL_fw_desc" "Distruggi il mietiore nemico e proteggi il tuo" "PL_fw_abbr" "FW" "GAMEMODE_kr" "Killrace Amplificata" @@ -198,10 +198,10 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "KR_NEW_RACER" "%s1 è il killracer amplificato" "KR_YOU_ARE_NEW_RACER" "Sei il killracer amplificato" "KR_YOU_SET_NEW_RECORD" "Stabilisci un nuovo record di uccisioni!" - "KR_FLAG_INCOMING" "Bandiera in Arrivo!" + "KR_FLAG_INCOMING" "Bandiera in Arrivo" "KR_COLLECT_FLAG" "Raccoglila per diventare il killracer!" - "KR_ENEMY_KILLRACE_OVER" "La killrace di %s1 è finita." - "KR_YOUR_KILLRACE_OVER" "La tua killrace è finita." + "KR_ENEMY_KILLRACE_OVER" "La killrace di %s1 è finita" + "KR_YOUR_KILLRACE_OVER" "La tua killrace è finita" "KR_YOUR_KILLRACE_SCORE" "Hai ottenuto %s1 uccisioni." "GAMEMODE_fastball" "Fastball" @@ -222,7 +222,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "custom_air_accel_pilot" "Accelerazione Aerea" "no_pilot_collision" "Collisione tra Piloti" "promode_enable" "Armi Modalità Competitiva" - "fp_embark_enabled" "Imbarchi/esecuzioni in 1ºpers." + "fp_embark_enabled" "Imbarchi/esecuzioni in 1º persona" "classic_rodeo" "Rodeo classico" "oob_timer_enabled" "Timer Fuori dai Limiti" "riff_instagib" "Modalità Instagib" @@ -250,7 +250,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "SP_CRASHSITE_CLASSIC_DESC" "Jack Cooper incontra BT-7274." "SP_SEWERS1" "Sangue e Ruggine" - "SP_SEWERS1_CLASSIC_DESC" "Cooper e BT cercano di raggiungere il Major Anderson." + "SP_SEWERS1_CLASSIC_DESC" "Cooper e BT cercano di raggiungere il Maggiore Anderson." "SP_BOOMTOWN_START" "Nell'abisso" "SP_BOOMTOWN_START_CLASSIC_DESC" "Una scorciatoia sotterranea porta a conseguenze impreviste." @@ -262,7 +262,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "SP_BEACON_CLASSIC_DESC" "Cooper e BT tentano di informare la flotta rimanente dei piani dell'IMC." "SP_TDAY" "Prova del fuoco" - "SP_TDAY_CLASSIC_DESC" "Le abilità del Titan di Cooper vengono messe alla prova in una battaglia senza quartiere per la cattura dell'Arca." + "SP_TDAY_CLASSIC_DESC" "Le abilità del Titan di Cooper vengono messe alla prova in una battaglia senza quartiere per la cattura dell'Arca" "SP_S2S" "L'Arca" "SP_S2S_CLASSIC_DESC" "Cooper e BT inseguono l'Arca affrontando una nave dopo l'altra." @@ -272,7 +272,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo // Better.Serverbrowser "SERVERS_COLUMN" "Server" - "PLAYERS_COLUMN" "Players" + "PLAYERS_COLUMN" "Giocatori" "MAP_COLUMN" "Mappa" "GAMEMODE_COLUMN" "Modalità" "REGION_COLUMN" "Regione" @@ -306,18 +306,79 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "HIDE_LOCKED" "Nascondi bloccati" // In-game chat - "HUD_CHAT_WHISPER_PREFIX" "[WHISPER]" + "HUD_CHAT_WHISPER_PREFIX" [BISBIGLIO]" "HUD_CHAT_SERVER_PREFIX" "[SERVER]" "NO_GAMESERVER_RESPONSE" "Non è stato possibile raggiungere il server" "BAD_GAMESERVER_RESPONSE" "Il server ha dato una risposta invalida" "UNAUTHORIZED_GAMESERVER" "Il server non è autorizzato a fare quella richiesta" - "UNAUTHORIZED_GAME" "Stryder non è riuscito a confermare che questo account possiede Titanfall 2" + "UNAUTHORIZED_GAME" "Stryder non è riuscito a confermare che questo account possieda Titanfall 2" "UNAUTHORIZED_PWD" "Password errata" - "STRYDER_RESPONSE" "Non è stato possibile analizzare la risposta Stryder" + "STRYDER_RESPONSE" "Non è stato possibile analizzare la risposta di Stryder" "PLAYER_NOT_FOUND" "Non è stato trovato l'account player" "INVALID_MASTERSERVER_TOKEN" "Token Masterserver invalido o scaduto" - "JSON_PARSE_ERROR" "Errore nell'analisi di risposta json" + "JSON_PARSE_ERROR" "Errore nell'analisi della risposta json" "UNSUPPORTED_VERSION" "La versione che stai usando non è più supportata" + + "MOD_SETTINGS" "Impostazioni Mod" + "NORTHSTAR_BASE_SETTINGS" "Impostazioni base Northstar" + "ONLY_HOST_MATCH_SETTINGS" "Solo l'Host può modificare le impostazioni della Partita Privata" + "ONLY_HOST_CAN_START_MATCH" "Solo l'Host può Iniziare la Partita" + "MATCH_COUNTDOWN_LENGTH" "Durata Countdown della Partita Privata" + "LOG_UNKNOWN_CLIENTCOMMANDS" "Registra Comandi Client Sconosciuti" + "DISALLOWED_TACTICALS" "Abilità Proibite" + "TACTICAL_REPLACEMENT" "Sostituzione Abilità" + "DISALLOWED_WEAPONS" "Armi Proibite" + "REPLACEMENT_WEAPON" "Sostituzione Armi" + "SHOULD_RETURN_TO_LOBBY" "Ritorna alla Lobby dopo Fine Partita" + "ARE_YOU_SURE" "Sei sicuro?" + "WILL_RESET_ALL_SETTINGS" "Questo ripristinerà TUTTE le impostazioni che appartengono a questa categoria.\n\nNON può essere annullato." + "WILL_RESET_SETTING" "Questo ripristinerà l'impostazione %s1 al suo valore predefinito.\n\nNON è reversibile." // obviously, don't translate %s1. + "MOD_SETTINGS_SERVER" "Server" + "MOD_SETTINGS_RESET" "Ripristina" + "MOD_SETTINGS_RESET_ALL" "Ripristina Tutto" + "HUD_CHAT_WHISPER_PREFIX" "[WHISPER]" + "NO_RESULTS" "Nessun Risultato." + "DISABLE" "Disattiva" + "DIALOG_AUTHENTICATING_MASTERSERVER" "Autenticazione al master server." + "NS_SERVERBROWSER_UNKNOWNMODE" "Modalità sconosciuta" + "respawnprotection" "Tempo di protezione al respawn" + "NO_MODS" "Nessuna impostazione disponibile! Installa più mods a ^5588FF00northstar.thunderstore.io^0." + "gg_assist_reward" "Percentuale ricompensa per assist" + "gg_execution_reward" "Percentuale ricompensa per esecuzione" + "PL_sns" "Sticks and Stones" + "PL_sns_lobby" "Lobby: Sticks and Stones" + "PL_sns_abbr" "SNS" + "GAMEMODE_SNS" "Sticks and Stones" + "SCOREBOARD_BANKRUPTS" "Uccisioni bancarotta" + "SNS_LEADER_BANKRUPT" "Leader punteggio in bancarotta!" + "SNS_LEADER_BANKRUPT_SUB" "%s1 è stato resettato da %s2" + "SNS_BANKRUPT" "Bancarotta!" + "sns_softball_kill_value" "Valore per uccisione Softball" + "sns_offhand_kill_value" "Valore per uccisione manuale" + "sns_melee_kill_value" "Valore per uccisione corpo a corpo" + "sns_softball_enabled" "Softball abilitato" + "PL_tffa" "Tutti contro tutti Titan" + "PL_tffa_lobby" "Lobby: Tutti contro tutti Titan" + "PL_tffa_hint" "Ogni pilota per sè, distruggi tutti i titan nemici." + "PL_tffa_abbr" "TFFA" + "GAMEMODE_TFFA" "Tutti contro tutti Titan" + "sns_reset_pulse_blade_cooldown_on_pulse_blade_kill" "Cooldown reset all'uccisione" + "SHOW" "Mostra" + "SHOW_ALL" "Tutto" + "SHOW_ONLY_ENABLED" "Solo Abilitate" + "SHOW_ONLY_DISABLED" "Solo Disabilitate" + "SHOW_ONLY_NOT_REQUIRED" "Solo Mods opzionali" + "SHOW_ONLY_REQUIRED" "Solo Mods richieste" + "WARNING" "Attenzione" + "CORE_MOD_DISABLE_WARNING" "Disattivare mods di base può rompere il client!" + "AUTHENTICATIONAGREEMENT_NO" "Hai scelto di non autenticarti con Northstar. Puoi vedere l'accordo nel menu delle Mods." + "aitdm_archer_grunts" "Soldati Archer" + "gg_kill_reward" "Percentuale ricompensa per uccisione" + "PL_sns_desc" "Tutti contro tutti. Usa la Lama Impulsi e l'esecuzione per resettare il punteggio avversario" + "SNS_BANKRUPT_SUB" "Il you punteggio è stato resettato da %s1" + "sns_wme_kill_value" "Valore per uccisione Wingman d'Elite" + "sns_reset_kill_value" "Valore per uccisione Lama Impulsi/Esecuzione" + "PL_tffa_desc" "Ogni pilota per sè, distruggi tutti i titan nemici." } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt index 9ce0c2e3e..fe7cf2b35 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt @@ -253,5 +253,11 @@ "INVALID_MASTERSERVER_TOKEN" "Срок действия жетона главного сервера истек или не является правильным" "JSON_PARSE_ERROR" "Ошибка разбора ответа json" "UNSUPPORTED_VERSION" "Используемая вами версия больше не поддерживается" + "DISABLE" "Выключить" + "DIALOG_AUTHENTICATING_MASTERSERVER" "Аутентификация на главном сервере." + "WARNING" "Предупреждение" + "CORE_MOD_DISABLE_WARNING" "Выключение главных модов может сломать ваш клиент!" + "AUTHENTICATIONAGREEMENT_NO" "Вы выбрали не аутентифицироваться с Northstar-ом. Вы можете посмотреть соглашение в меню модов." + "NS_SERVERBROWSER_UNKNOWNMODE" "Неизвестный режим" } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_tchinese.txt b/Northstar.Client/mod/resource/northstar_client_localisation_tchinese.txt index 12b6cad1b..e543f7117 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_tchinese.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_tchinese.txt @@ -127,7 +127,7 @@ "PL_sns" "冷兵器" "PL_sns_lobby" "冷兵器大廳" - "PL_sns_desc" "模擬十字弓和飛斧的作戰模式。使用脈衝刀及處決擊殺來重置敵人的分數。" + "PL_sns_desc" "无规则. 使用脈衝刀及處決擊殺來重置敵人的分數" "PL_sns_abbr" "SNS" "GAMEMODE_SNS" "冷兵器" "SCOREBOARD_BANKRUPTS" "破產次數" @@ -155,7 +155,7 @@ "INFECTION_LAST_SURVIVOR" "%s1是最後的倖存者!" "INFECTION_KILL_LAST_SURVIVOR" "在時間用盡前感染他!" "INFECTION_YOU_ARE_LAST_SURVIVOR" "你是最後的倖存者!" - "INFECTION_SURVIVE_LAST_SURVIVOR" "倖存" + "INFECTION_SURVIVE_LAST_SURVIVOR" "最後幸存者." "PL_tffa" "泰坦混戰" "PL_tffa_lobby" "泰坦混戰大廳" @@ -238,38 +238,38 @@ "player_bleedout_aiBleedingPlayerMissChance" "倒地時AI的失誤率" // coop stuff - "PL_sp_coop" "(UNFINISHED) Singleplayer Coop" - "PL_sp_coop_lobby" "Singleplayer Coop Lobby" - "PL_sp_coop_desc" "Play through the singleplayer campaign with friends" - "PL_sp_coop_hint" "Play through the singleplayer campaign with friends" + "PL_sp_coop" "(未完成) 單人合作" + "PL_sp_coop_lobby" "單人合作模式大廳" + "PL_sp_coop_desc" "與朋友一起遊玩單人戰役" + "PL_sp_coop_hint" "與朋友一起遊玩單人戰役" "PL_sp_coop_abbr" "SP" - "SP_TRAINING" "The Pilot's Gauntlet" - "SP_TRAINING_CLASSIC_DESC" "Captain Lastimosa's training simulation." + "SP_TRAINING" "鐵御的試煉" + "SP_TRAINING_CLASSIC_DESC" "拉絲提莫沙上尉的模擬訓練." "SP_CRASHSITE" "BT-7274" - "SP_CRASHSITE_CLASSIC_DESC" "Jack Cooper meets BT-7274." + "SP_CRASHSITE_CLASSIC_DESC" "傑克庫博預見BT-7274." - "SP_SEWERS1" "Blood and Rust" - "SP_SEWERS1_CLASSIC_DESC" "Cooper and BT set out to rendezvous with Major Anderson." + "SP_SEWERS1" "鮮血與鐵鏽" + "SP_SEWERS1_CLASSIC_DESC" "庫博和BT一起出發前往與安德森上尉回合." - "SP_BOOMTOWN_START" "Into the Abyss" - "SP_BOOMTOWN_START_CLASSIC_DESC" "An underground shortcut yields unexpected consequences." + "SP_BOOMTOWN_START" "踏入虛空" + "SP_BOOMTOWN_START_CLASSIC_DESC" "一個地下捷徑造成了不可預見的後果." - "SP_HUB_TIMESHIFT" "Effect and Cause" - "SP_HUB_TIMESHIFT_CLASSIC_DESC" "A strange phenomenon is discovered at Major Anderson's coordinates." + "SP_HUB_TIMESHIFT" "因果報應" + "SP_HUB_TIMESHIFT_CLASSIC_DESC" "在安德森上尉的坐標處發現了奇異的現象." - "SP_BEACON" "The Beacon" - "SP_BEACON_CLASSIC_DESC" "Cooper and BT attempt to inform the remaining fleet of the IMC's plans." + "SP_BEACON" "信號台" + "SP_BEACON_CLASSIC_DESC" "庫博和BT嘗試將IMC的計劃通知給剩餘反抗軍艦隊." - "SP_TDAY" "Trial by Fire" - "SP_TDAY_CLASSIC_DESC" "Cooper's Titan skills are put to the test in an all-out battle to capture the Ark" + "SP_TDAY" "烈火審判" + "SP_TDAY_CLASSIC_DESC" "庫博驾馭泰坦的技術在爭奪聖櫃的全面戰爭中得到考驗" - "SP_S2S" "The Ark" - "SP_S2S_CLASSIC_DESC" "Cooper and BT go ship to ship in pursuit of the Ark." + "SP_S2S" "聖櫃" + "SP_S2S_CLASSIC_DESC" "庫博和BT在艦艇中穿梭追尋聖櫃." - "SP_SKYWAY_V1" "The Fold Weapon" - "SP_SKYWAY_V1_CLASSIC_DESC" "BT and Cooper are captured by Kuben Blisk." + "SP_SKYWAY_V1" "折疊時空武器" + "SP_SKYWAY_V1_CLASSIC_DESC" "BT和庫博被庫本布里斯克擒拿." // Better.Serverbrowser "SERVERS_COLUMN" "伺服器" @@ -310,8 +310,8 @@ "HUD_CHAT_SERVER_PREFIX" "[伺服器]" // In-game chat - "HUD_CHAT_WHISPER_PREFIX" "[WHISPER]" - "HUD_CHAT_SERVER_PREFIX" "[SERVER]" + "HUD_CHAT_WHISPER_PREFIX" "[悄悄話]" + "HUD_CHAT_SERVER_PREFIX" "[伺服器]" "NO_GAMESERVER_RESPONSE" "無法連接到遊戲伺服器'" "BAD_GAMESERVER_RESPONSE" "遊戲伺服器回應無效" @@ -323,5 +323,26 @@ "INVALID_MASTERSERVER_TOKEN" "主伺服器token過期或無效" "JSON_PARSE_ERROR" "讀取json回應時發生錯誤" "UNSUPPORTED_VERSION" "您的遊戲版本過低" + "NORTHSTAR_BASE_SETTINGS" "北极星基础设置" + "ONLY_HOST_CAN_START_MATCH" "只有服主可以開始對局" + "MATCH_COUNTDOWN_LENGTH" "私人對局倒計時時間" + "DISALLOWED_TACTICALS" "禁用的技能" + "TACTICAL_REPLACEMENT" "替換的技能" + "NO_RESULTS" "無結果." + "LOG_UNKNOWN_CLIENTCOMMANDS" "登記未知客戶端指令" + "SHOULD_RETURN_TO_LOBBY" "對局結束後返回大廳" + "ARE_YOU_SURE" "確定?" + "WILL_RESET_SETTING" "這將會重置 %s1 的設置為默認值.\n\n此操作不可復原." + "MOD_SETTINGS_SERVER" "服務器" + "MOD_SETTINGS_RESET" "重置" + "MOD_SETTINGS_RESET_ALL" "重置所有" + "SHOW_ONLY_NOT_REQUIRED" "僅展示非必需模組" + "SHOW_ONLY_REQUIRED" "僅展示必須模組" + "MOD_SETTINGS" "模组设置" + "ONLY_HOST_MATCH_SETTINGS" "只有服主可以修改私人對局設置" + "DISALLOWED_WEAPONS" "禁用的武器" + "REPLACEMENT_WEAPON" "替换的武器" + "WILL_RESET_ALL_SETTINGS" "這將會重置所有屬於改條目的設置.\n\n此操作不可復原." + "NO_MODS" "無可用模組! 前往 ^5588FF00northstar.thunderstore.io^0 下載更多." } } diff --git a/Northstar.Client/mod/resource/ui/menus/mod_settings.menu b/Northstar.Client/mod/resource/ui/menus/mod_settings.menu new file mode 100644 index 000000000..2fed2bd18 --- /dev/null +++ b/Northstar.Client/mod/resource/ui/menus/mod_settings.menu @@ -0,0 +1,511 @@ +"resource/ui/menus/mods_browse.menu" +{ + "menu" + { + "ControlName" "Frame" + "xpos" "0" + "ypos" "0" + "zpos" "3" + "wide" "f0" + "tall" "f0" + "autoResize" "1" + "visible" "1" + "enabled" "1" + "pinCorner" "0" + "PaintBackgroundType" "0" + "infocus_bgcolor_override" "0 0 0 0" + "outoffocus_bgcolor_override" "0 0 0 0" + "Vignette" + { + "ControlName" "ImagePanel" + "InheritProperties" "MenuVignette" + } + "Title" + { + "ControlName" "Label" + "InheritProperties" "MenuTitle" + "labelText" "#MOD_SETTINGS" + } + "ImgTopBar" + { + "ControlName" "ImagePanel" + "InheritProperties" "MenuTopBar" + } + "DarkenBackground" + { + "ControlName" "Label" + "classname" "ConnectingHUD" + "xpos" "0" + "ypos" "0" + "zpos" "99" + "wide" "%100" + "tall" "%100" + "labelText" "" + "bgcolor_override" "0 0 0 0" + "visible" "0" + "paintbackground" "1" + } + "ButtonRowAnchor" + { + "ControlName" "Label" + "labelText" "" + "pin_to_sibling" "DarkenBackground" + "pin_to_sibling_corner" "TOP_LEFT" + "pin_corner_to_sibling" "BOTTOM_LEFT" + "xpos" "-150" + "ypos" "-200" + } + "FilterButtonsRowAnchor" + { + "ControlName" "Label" + "pin_to_sibling" "LabelDetails" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "pin_corner_to_sibling" "TOP_LEFT" + "labelText" "" + "ypos" "12" + } + "NoResultLabel" + { + "ControlName" "Label" + "xpos" "0" + ypos "0" + wide "1200" + tall "675" + //auto_tall_tocontents 1 + visible "1" + enabled "1" + //auto_wide_tocontents 1 + labelText "No results." + textAlignment "center" + //auto_wide_tocontents "1" + //auto_tall_tocontents "1" + //fgcolor_override "255 255 255 255" + //bgcolor_override "0 0 0 200" + font Default_41 + + pin_to_sibling ButtonRowAnchor + pin_to_sibling_corner TOP_LEFT + pin_corner_to_sibling TOP_LEFT + } + // pain // + "BtnMod1" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "pin_to_sibling" "ButtonRowAnchor" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_LEFT" + "navUp" "BtnMod15" + "navDown" "BtnMod2" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "scriptID" "0" + } + "BtnMod2" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "pin_to_sibling" "BtnMod1" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod1" + "navDown" "BtnMod3" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "scriptID" "1" + } + "BtnMod3" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "2" + "pin_to_sibling" "BtnMod2" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod2" + "navDown" "BtnMod4" + } + "BtnMod4" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "3" + "pin_to_sibling" "BtnMod3" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod3" + "navDown" "BtnMod5" + } + "BtnMod5" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "4" + "pin_to_sibling" "BtnMod4" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod4" + "navDown" "BtnMod6" + } + "BtnMod6" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "5" + "pin_to_sibling" "BtnMod5" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod5" + "navDown" "BtnMod7" + } + "BtnMod7" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "6" + "pin_to_sibling" "BtnMod6" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod6" + "navDown" "BtnMod8" + } + "BtnMod8" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "7" + "pin_to_sibling" "BtnMod7" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod7" + "navDown" "BtnMod9" + } + "BtnMod9" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "8" + "pin_to_sibling" "BtnMod8" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod8" + "navDown" "BtnMod10" + } + "BtnMod10" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "9" + "pin_to_sibling" "BtnMod9" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod9" + "navDown" "BtnMod11" + } + "BtnMod11" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "10" + "pin_to_sibling" "BtnMod10" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod10" + "navDown" "BtnMod12" + } + "BtnMod12" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "11" + "pin_to_sibling" "BtnMod11" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod11" + "navDown" "BtnMod13" + } + "BtnMod13" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "12" + "pin_to_sibling" "BtnMod12" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod12" + "navDown" "BtnMod14" + } + "BtnMod14" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "13" + "pin_to_sibling" "BtnMod13" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod13" + "navDown" "BtnMod15" + } + "BtnMod15" + { + "ControlName" "CNestedPanel" + "classname" "ModButton" + "tall" "45" + "wide" "1200" + "controlSettingsFile" "resource/UI/menus/panels/mod_setting.res" + "classname" "ModButton" + "scriptID" "14" + "pin_to_sibling" "BtnMod14" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + "navUp" "BtnMod14" + "navDown" "BtnMod1" + } + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + "FilterPanel" + { + "ControlName" "RuiPanel" + "wide" "1220" + "tall" "112" + //"xpos" "-8" + "classname" "FilterPanelChild" + "rui" "ui/knowledgebase_panel.rpak" + "visible" "1" + "zpos" "-1" + "pin_to_sibling" "FilterButtonsRowAnchor" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "LabelDetails" + { + "ControlName" "RuiPanel" + "tall" "695" + "wide" "1220" + "xpos" "10" + "ypos" "10" + "pin_to_sibling" "ButtonRowAnchor" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_LEFT" + "rui" "ui/knowledgebase_panel.rpak" + "wrap" "1" + "visible" "1" + "zpos" "-1" + } + "BtnSearchLabel" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + "labelText" "#SEARCHBAR_LABEL" + "textAlignment" "west" + "classname" "FilterPanelChild" + "wide" "500" + "xpos" "-23" + "ypos" "-16" + "wrap" "1" + "visible" "1" + "zpos" "0" + "pin_to_sibling" "FilterButtonsRowAnchor" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "BtnModsSearch" + { + "ControlName" "TextEntry" + "classname" "FilterPanelChild" + "zpos" "100" // This works around input weirdness when the control is constructed by code instead of VGUI blackbox. + "xpos" "-400" + "ypos" "-5" + "wide" "390" + "tall" "30" + "textHidden" "0" + "editable" "1" + "font" "Default_21" + "allowRightClickMenu" "0" + "allowSpecialCharacters" "1" + "unicode" "1" + "pin_to_sibling" "BtnSearchLabel" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_RIGHT" + } + "BtnFiltersClear" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + "labelText" "#CLEAR_FILTERS" + "classname" "FilterPanelChild" + "wide" "100" + "xpos" "0" + "ypos" "0" + "zpos" "90" + "scriptID" "999" + "pin_to_sibling" "BtnSearchLabel" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_RIGHT" + } + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + "BtnModListUpArrow" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + // labelText "A"F + "wide" "40" + "tall" "40" + "xpos" "2" + "ypos" "0" + "image" "vgui/hud/white" + "drawColor" "255 255 255 128" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "BtnModListUpArrowPanel" + { + "ControlName" "RuiPanel" + "wide" "40" + "tall" "40" + "xpos" "2" + "ypos" "0" + "rui" "ui/knowledgebase_panel.rpak" + "visible" "1" + "zpos" "-1" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "BtnModListDownArrow" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + // labelText "V" + "wide" "40" + "tall" "40" + "xpos" "2" + "ypos" "-655" + "image" "vgui/hud/white" + "drawColor" "255 255 255 128" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "BtnModListDownArrowPanel" + { + "ControlName" "RuiPanel" + "wide" "40" + "tall" "40" + "xpos" "2" + "ypos" "-655" + "rui" "ui/knowledgebase_panel.rpak" + "visible" "1" + "zpos" "-1" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "BtnModListSlider" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + // labelText "V" + "wide" "40" + "tall" "420" + "xpos" "2" + "ypos" "-40" + "zpos" "0" + "image" "vgui/hud/white" + "drawColor" "255 255 255 128" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "BtnModListSliderPanel" + { + "ControlName" "RuiPanel" + "wide" "40" + "tall" "420" + "xpos" "2" + "ypos" "-40" + "rui" "ui/knowledgebase_panel.rpak" + "visible" "1" + "zpos" "-1" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + // sh_menu_models.gnut has a global function which gets called when + // left mouse button gets called while hovering and has mouse + // deltaX; deltaY which we can yoink for ourselfes + "MouseMovementCapture" + { + "ControlName" "CMouseMovementCapturePanel" + "wide" "40" + "tall" "604" + "xpos" "2" + "ypos" "-40" + "zpos" "1" + "pin_to_sibling" "LabelDetails" + "pin_corner_to_sibling" "TOP_RIGHT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "ButtonTooltip" + { + "ControlName" "CNestedPanel" + "InheritProperties" "ButtonTooltip" + } + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + "FooterButtons" + { + "ControlName" "CNestedPanel" + "InheritProperties" "FooterButtons" + } + } +} diff --git a/Northstar.Client/mod/resource/ui/menus/panels/mod_setting.res b/Northstar.Client/mod/resource/ui/menus/panels/mod_setting.res new file mode 100644 index 000000000..92dce922d --- /dev/null +++ b/Northstar.Client/mod/resource/ui/menus/panels/mod_setting.res @@ -0,0 +1,183 @@ +"resource/ui/menus/panels/mod_setting.res" +{ + "FULL" + { + "ControlName" "Label" + "classname" "ConnectingHUD" + "xpos" "0" + "ypos" "0" + "zpos" "99" + "wide" "1200" + "tall" "45" + "labelText" "" + "bgcolor_override" "0 0 0 0" + "visible" "0" + "paintbackground" "1" + } + "BtnMod" + { + "ControlName" "Label" + "InheritProperties" "RuiSmallButton" + "labelText" "Mod" + //"auto_wide_tocontents" "1" + "navRight" "EnumSelectButton" + "navLeft" "TextEntrySetting" + "wide" "390" + "tall" "45" + } + // we're getting to the top of this :) + "TopLine" + { + "ControlName" "ImagePanel" + "InheritProperties" "MenuTopBar" + "ypos" "0" + "wide" "%100" + "pin_to_sibling" "BtnMod" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_LEFT" + } + "ModTitle" + { + "ControlName" "Label" + "InheritProperties" "RuiSmallButton" + "labelText" "Mod" + "font" "DefaultBold_43" + //"auto_wide_tocontents" "1" + "zpos" "-999" + "textAlignment" "center" + "navRight" "EnumSelectButton" + "navLeft" "TextEntrySetting" + "wide" "1200" + "tall" "45" + + } + "Slider" + { + "ControlName" "SliderControl" + //"InheritProperties" "RuiSmallButton" + minValue 0.0 + maxValue 2.0 + stepSize 0.05 + "pin_to_sibling" "BtnMod" + "pin_corner_to_sibling" "TOP_LEFT" + "pin_to_sibling_corner" "TOP_RIGHT" + "navRight" "ResetModToDefault" + "navLeft" "TextEntrySetting" + //isValueClampedToStepSize 1 + BtnDropButton + { + ControlName RuiButton + //InheritProperties WideButton + style SliderButton + "wide" "320" + "tall" "45" + "labelText" "" + "auto_wide_tocontents" "0" + } + "wide" "320" + "tall" "45" + } + "EnumSelectButton" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + "style" "DialogListButton" + "labelText" "" + "zpos" "4" + "wide" "225" + "tall" "45" + //"xpos" "10" + "scriptID" "0" + "pin_to_sibling" "FULL" + "pin_corner_to_sibling" "RIGHT" + "pin_to_sibling_corner" "RIGHT" + "navLeft" "ResetModToDefault" + "navRight" "TextEntrySetting" + } + "ResetModToDefault" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + "labelText" "" + "zpos" "0" + "xpos" "10" + "wide" "45" + "tall" "45" + "scriptID" "0" + "pin_to_sibling" "EnumSelectButton" + "pin_corner_to_sibling" "RIGHT" + "pin_to_sibling_corner" "LEFT" + "navLeft" "Slider" + "navRight" "TextEntrySetting" + } + "ResetModImage" + { + "ControlName" "ImagePanel" + "image" "vgui/reset" + "scaleImage" "1" + "drawColor" "180 180 180 255" // vanilla label color + "visible" "0" + "wide" "30" + "tall" "30" + "enabled" "0" + + "pin_to_sibling" "ResetModToDefault" + "pin_corner_to_sibling" "CENTER" + "pin_to_sibling_corner" "CENTER" + } + "OpenCustomMenu" + { + "ControlName" "RuiButton" + "InheritProperties" "RuiSmallButton" + "labelText" "Open" + //"auto_wide_tocontents" "1" + "zpos" "4" + "wide" "1200" + "textAlignment" "center" + //"font" "Default_41" + //"xpos" "10" + "tall" "40" + "scriptID" "0" + "visible" "0" + "pin_to_sibling" "FULL" + "pin_corner_to_sibling" "RIGHT" + "pin_to_sibling_corner" "RIGHT" + "navLeft" "TextEntrySetting" + "navRight" "TextEntrySetting" + } + "TextEntrySetting" + { + "ControlName" "TextEntry" + "classname" "MatchSettingTextEntry" + //"xpos" "-35" + //"ypos" "-5" + "zpos" "100" // This works around input weirdness when the control is constructed by code instead of VGUI blackbox. + "wide" "160" + "tall" "30" + "scriptID" "0" + "textHidden" "0" + "editable" "1" + // NumericInputOnly 1 + "font" "Default_21" + "allowRightClickMenu" "0" + "allowSpecialCharacters" "1" + "unicode" "0" + "pin_to_sibling" "EnumSelectButton" + "pin_corner_to_sibling" "CENTER" + "pin_to_sibling_corner" "CENTER" + "navLeft" "EnumSelectButton" + "navRight" "EnumSelectButton" + } + // we're getting to the bottom of this :) + "BottomLine" + { + "ControlName" "ImagePanel" + "InheritProperties" "MenuTopBar" + "ypos" "5" + "wide" "%100" + //"tall" "0" + "pin_to_sibling" "FULL" + "pin_corner_to_sibling" "BOTTOM_LEFT" + "pin_to_sibling_corner" "BOTTOM_LEFT" + } +} diff --git a/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut index 212568d0e..2a2ed3dbe 100644 --- a/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut +++ b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut @@ -19,4 +19,25 @@ global struct UIPresenceStruct { bool isLobby string loadingLevel string loadedLevel -} \ No newline at end of file +} + +global struct RequiredModInfo +{ + string name + string version +} + +global struct ServerInfo +{ + int index + string id + string name + string description + string map + string playlist + int playerCount + int maxPlayerCount + bool requiresPassword + string region + array< RequiredModInfo > requiredMods +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut index 03bd89595..35c9e9bae 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut @@ -112,8 +112,13 @@ void function InitInGameMPMenu() Hud_AddEventHandler( soundButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "VideoMenu" ) ) ) #endif - file.faqButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#KNB_MENU_HEADER" ) - Hud_AddEventHandler( file.faqButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "KnowledgeBaseMenu" ) ) ) + // MOD SETTINGS + var modSettingsButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "Mod Settings" ) + Hud_AddEventHandler( modSettingsButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "ModSettings" ) ) ) + + // Nobody reads the FAQ so we replace it with ModSettings because of the limited combobutton space available + //file.faqButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#KNB_MENU_HEADER" ) + //Hud_AddEventHandler( file.faqButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "KnowledgeBaseMenu" ) ) ) //var dataCenterButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#DATA_CENTER" ) //Hud_AddEventHandler( dataCenterButton, UIE_CLICK, OpenDataCenterDialog ) @@ -133,7 +138,7 @@ void function OnInGameMPMenu_Open() bool faqIsNew = !GetConVarBool( "menu_faq_viewed" ) || HaveNewPatchNotes() || HaveNewCommunityNotes() RuiSetBool( Hud_GetRui( file.settingsHeader ), "isNew", faqIsNew ) - ComboButton_SetNew( file.faqButton, faqIsNew ) + //ComboButton_SetNew( file.faqButton, faqIsNew ) UpdateLoadoutButtons() RefreshCreditsAvailable() @@ -255,6 +260,10 @@ void function InitInGameSPMenu() var videoButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#VIDEO" ) Hud_AddEventHandler( videoButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "VideoMenu" ) ) ) #endif + + // MOD SETTINGS + var modSettingsButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "Mod Settings" ) + Hud_AddEventHandler( modSettingsButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "ModSettings" ) ) ) array orderedButtons diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_lobby.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_lobby.nut index 3c868aab2..2bef0e205 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_lobby.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_lobby.nut @@ -352,8 +352,9 @@ void function SetupComboButtonTest( var menu ) var soundButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#VIDEO" ) Hud_AddEventHandler( soundButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "VideoMenu" ) ) ) #endif - file.faqButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#KNB_MENU_HEADER" ) - Hud_AddEventHandler( file.faqButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "KnowledgeBaseMenu" ) ) ) + // MOD SETTINGS + var modSettingsButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#MOD_SETTINGS" ) + Hud_AddEventHandler( modSettingsButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "ModSettings" ) ) ) comboStruct.navUpButtonDisabled = true comboStruct.navDownButton = file.genUpButton @@ -635,9 +636,9 @@ void function OnLobbyMenu_Open() ComboButton_SetNew( file.factionButton, anyNewFactions ) } - bool faqIsNew = !GetConVarBool( "menu_faq_viewed" ) || HaveNewPatchNotes() || HaveNewCommunityNotes() + /*bool faqIsNew = !GetConVarBool( "menu_faq_viewed" ) || HaveNewPatchNotes() || HaveNewCommunityNotes() RuiSetBool( Hud_GetRui( file.settingsHeader ), "isNew", faqIsNew ) - ComboButton_SetNew( file.faqButton, faqIsNew ) + ComboButton_SetNew( file.faqButton, faqIsNew )*/ TryUnlockSRSCallsign() diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_mod_settings.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_mod_settings.nut new file mode 100644 index 000000000..a45082c71 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_mod_settings.nut @@ -0,0 +1,1105 @@ +untyped +global function AddModSettingsMenu +global function ModSettings_AddSetting +global function ModSettings_AddEnumSetting +global function ModSettings_AddSliderSetting +global function ModSettings_AddButton +global function ModSettings_AddModTitle +global function ModSettings_AddModCategory +global function PureModulo + +// Legacy functions for backwards compatability. These will be removed eventually +global function AddConVarSetting +global function AddConVarSettingEnum +global function AddConVarSettingSlider +global function AddModSettingsButton +global function AddModTitle +global function AddModCategory + +const int BUTTONS_PER_PAGE = 15 +const string SETTING_ITEM_TEXT = " " // this is long enough to be the same size as the textentry field + +enum eEmptySpaceType +{ + None, + TopBar, + BottomBar +} + +struct ConVarData { + string displayName + bool isEnumSetting = false + string conVar + string type + + string modName + string catName + bool isCategoryName = false + bool isModName = false + + bool isEmptySpace = false + int spaceType = 0 + + // SLIDER BULLSHIT + bool sliderEnabled = false + float min = 0.0 + float max = 1.0 + float stepSize = 0.05 + bool forceClamp = false + + bool isCustomButton = false + void functionref() onPress + + array values + var customMenu + bool hasCustomMenu = false +} + +struct { + var menu + int scrollOffset = 0 + bool updatingList = false + bool isOpen + + array conVarList + // if people use searches - i hate them but it'll do : ) + array filteredList + string filterText = "" + table enumRealValues + table setFuncs + array modPanels + array resetModButtons + array sliders + string currentMod = "" + string currentCat = "" +} file + +struct { + int deltaX = 0 + int deltaY = 0 +} mouseDeltaBuffer + +void function AddModSettingsMenu() +{ + AddMenu( "ModSettings", $"resource/ui/menus/mod_settings.menu", InitModMenu ) +} + +void function InitModMenu() +{ + file.menu = GetMenu( "ModSettings" ) + // DumpStack(2) + AddMenuFooterOption( file.menu, BUTTON_B, "#B_BUTTON_BACK", "#BACK" ) + + ///////////////////////////// + // BASE NORTHSTAR SETTINGS // + ///////////////////////////// + + /* + ModSettings_AddModTitle( "^FF000000EXAMPLE" ) + ModSettings_AddModCategory( "I wasted way too much time on this..." ) + ModSettings_AddButton( "This is a custom button you can click on!", void function() : () + { + print( "HELLOOOOOO" ) + } ) + ModSettings_AddEnumSetting( "filter_mods", "Very Huge Enum Example", split( "Never gonna give you up Never gonna let you down Never gonna run around and desert you Never gonna make you cry Never gonna say goodbye Never gonna tell a lie and hurt you", " " ) ) + */ + // Nuke weird rui on filter switch :D + // RuiSetString( Hud_GetRui( Hud_GetChild( file.menu, "SwtBtnShowFilter" ) ), "buttonText", "" ) + + file.modPanels = GetElementsByClassname( file.menu, "ModButton" ) + + AddMenuEventHandler( file.menu, eUIEvent.MENU_OPEN, OnModMenuOpened ) + AddMenuEventHandler( file.menu, eUIEvent.MENU_CLOSE, OnModMenuClosed ) + + int len = file.modPanels.len() + for ( int i = 0; i < len; i++ ) + { + + // AddButtonEventHandler( button, UIE_CHANGE, OnSettingButtonPressed ) + // get panel + var panel = file.modPanels[i] + + // reset to default nav + var child = Hud_GetChild( panel, "BtnMod" ) + + + child.SetNavUp( Hud_GetChild( file.modPanels[ int( PureModulo( i - 1, len ) ) ], "BtnMod" ) ) + child.SetNavDown( Hud_GetChild( file.modPanels[ int( PureModulo( i + 1, len ) ) ], "BtnMod" ) ) + + // Enum button nav + child = Hud_GetChild( panel, "EnumSelectButton" ) + Hud_DialogList_AddListItem( child, SETTING_ITEM_TEXT, "main" ) + Hud_DialogList_AddListItem( child, SETTING_ITEM_TEXT, "next" ) + Hud_DialogList_AddListItem( child, SETTING_ITEM_TEXT, "prev" ) + + child.SetNavUp( Hud_GetChild( file.modPanels[ int( PureModulo( i - 1, len ) ) ], "EnumSelectButton" ) ) + child.SetNavDown( Hud_GetChild( file.modPanels[ int( PureModulo( i + 1, len ) ) ], "EnumSelectButton" ) ) + Hud_AddEventHandler( child, UIE_CLICK, UpdateEnumSetting ) + + // reset button nav + + child = Hud_GetChild( panel, "ResetModToDefault" ) + Hud_AddEventHandler( child, UIE_GET_FOCUS, void function( var child ) : (panel) + { + Hud_SetColor( Hud_GetChild( panel, "ResetModImage" ), 0, 0, 0, 255 ) + }) + Hud_AddEventHandler( child, UIE_LOSE_FOCUS, void function( var child ) : (panel) + { + Hud_SetColor( Hud_GetChild( panel, "ResetModImage" ), 180, 180, 180, 180 ) + }) + + child.SetNavUp( Hud_GetChild( file.modPanels[ int( PureModulo( i - 1, len ) ) ], "ResetModToDefault" ) ) + child.SetNavDown( Hud_GetChild( file.modPanels[ int( PureModulo( i + 1, len ) ) ], "ResetModToDefault" ) ) + + Hud_AddEventHandler( child, UIE_CLICK, ResetConVar ) + file.resetModButtons.append(child) + + // text field nav + child = Hud_GetChild( panel, "TextEntrySetting" ) + + Hud_AddEventHandler( child, UIE_LOSE_FOCUS, SendTextPanelChanges ) + + child.SetNavUp( Hud_GetChild( file.modPanels[ int( PureModulo( i - 1, len ) ) ], "TextEntrySetting" ) ) + child.SetNavDown( Hud_GetChild( file.modPanels[ int( PureModulo( i + 1, len ) ) ], "TextEntrySetting" ) ) + + child = Hud_GetChild( panel, "Slider" ) + + child.SetNavUp( Hud_GetChild( file.modPanels[ int( PureModulo( i - 1, len ) ) ], "Slider" ) ) + child.SetNavDown( Hud_GetChild( file.modPanels[ int( PureModulo( i + 1, len ) ) ], "Slider" ) ) + + file.sliders.append( MS_Slider_Setup( child ) ) + + Hud_AddEventHandler( child, UIE_CHANGE, OnSliderChange ) + + child = Hud_GetChild( panel, "OpenCustomMenu" ) + + Hud_AddEventHandler( child, UIE_CLICK, CustomButtonPressed ) + } + + // Hud_AddEventHandler( Hud_GetChild( file.menu, "BtnModsSearch" ), UIE_LOSE_FOCUS, OnFilterTextPanelChanged ) + Hud_AddEventHandler( Hud_GetChild( file.menu, "BtnFiltersClear" ), UIE_CLICK, OnClearButtonPressed ) + // mouse delta + AddMouseMovementCaptureHandler( file.menu, UpdateMouseDeltaBuffer ) + + Hud_AddEventHandler( Hud_GetChild( file.menu, "BtnModsSearch" ), UIE_CHANGE, void function ( var inputField ) : () + { + file.filterText = Hud_GetUTF8Text( inputField ) + OnFiltersChange() + } ) +} + +// "PureModulo" +// Used instead of modulo in some places. +// Why? beacuse PureModulo loops back onto positive numbers instead of going into the negatives. +// DO NOT TOUCH. +// a / b != floor( float( a ) / b ) +// int( float( a ) / b ) != floor( float( a ) / b ) +// Examples: +// -1 % 5 = -1 +// PureModulo( -1, 5 ) = 4 +float function PureModulo( int a, int b ) +{ + return b * ( ( float( a ) / b ) - floor( float( a ) / b ) ) +} + +void function ResetConVar( var button ) +{ + ConVarData conVar = file.filteredList[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset ] + + if ( conVar.isCategoryName ) + ShowAreYouSureDialog( "#ARE_YOU_SURE", ResetAllConVarsForModEventHandler( conVar.catName ), "#WILL_RESET_ALL_SETTINGS" ) + else + ShowAreYouSureDialog( "#ARE_YOU_SURE", ResetConVarEventHandler( int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset ), Localize( "#WILL_RESET_SETTING", Localize( conVar.displayName ) ) ) +} + +void function ShowAreYouSureDialog( string header, void functionref() func, string details ) +{ + DialogData dialogData + dialogData.header = header + dialogData.message = details + + AddDialogButton( dialogData, "#NO" ) + AddDialogButton( dialogData, "#YES", func ) + + AddDialogFooter( dialogData, "#A_BUTTON_SELECT" ) + AddDialogFooter( dialogData, "#B_BUTTON_BACK" ) + + OpenDialog( dialogData ) +} + +void functionref() function ResetAllConVarsForModEventHandler( string catName ) +{ + return void function() : ( catName ) + { + for ( int i = 0; i < file.conVarList.len(); i++ ) + { + ConVarData c = file.conVarList[ i ] + if ( c.catName != catName || c.isCategoryName || c.isEmptySpace || c.isCustomButton ) + continue + SetConVarToDefault( c.conVar ) + + int index = file.filteredList.find( c ) + if ( file.filteredList.find( c ) < 0 ) + continue + + if ( min( BUTTONS_PER_PAGE - 1, max( 0, index - file.scrollOffset ) ) == index - file.scrollOffset ) + { + Hud_SetText( Hud_GetChild( file.modPanels[ index - file.scrollOffset ], "TextEntrySetting" ), c.isEnumSetting ? c.values[ GetConVarInt( c.conVar ) ] : GetConVarString( c.conVar ) ) + if( c.sliderEnabled ) + MS_Slider_SetValue( file.sliders[ index - file.scrollOffset ], GetConVarFloat( c.conVar ) ) + } + } + } +} + +void functionref() function ResetConVarEventHandler( int modIndex ) +{ + return void function() : ( modIndex ) + { + ConVarData c = file.filteredList[ modIndex ] + SetConVarToDefault( c.conVar ) + if ( min( BUTTONS_PER_PAGE - 1, max( 0, modIndex - file.scrollOffset ) ) == modIndex - file.scrollOffset ) + { + Hud_SetText( Hud_GetChild( file.modPanels[ modIndex - file.scrollOffset ], "TextEntrySetting" ), c.isEnumSetting ? c.values[ GetConVarInt( c.conVar ) ] : GetConVarString( c.conVar ) ) + if( c.sliderEnabled ) + MS_Slider_SetValue( file.sliders[ modIndex - file.scrollOffset ], GetConVarFloat( c.conVar ) ) + } + } +} + +//////////// +// slider // +//////////// +void function UpdateMouseDeltaBuffer( int x, int y ) +{ + mouseDeltaBuffer.deltaX += x + mouseDeltaBuffer.deltaY += y + + SliderBarUpdate() +} + +void function FlushMouseDeltaBuffer() +{ + mouseDeltaBuffer.deltaX = 0 + mouseDeltaBuffer.deltaY = 0 +} + +void function SliderBarUpdate() +{ + if ( file.filteredList.len() <= 15 ) + { + FlushMouseDeltaBuffer() + return + } + + var sliderButton = Hud_GetChild( file.menu, "BtnModListSlider" ) + var sliderPanel = Hud_GetChild( file.menu, "BtnModListSliderPanel" ) + var movementCapture = Hud_GetChild( file.menu, "MouseMovementCapture" ) + + Hud_SetFocused( sliderButton ) + + float minYPos = -40.0 * ( GetScreenSize()[1] / 1080.0 ) // why the hardcoded positions?!?!?!?!?! + float maxHeight = 615.0 * ( GetScreenSize()[1] / 1080.0 ) + float maxYPos = minYPos - ( maxHeight - Hud_GetHeight( sliderPanel ) ) + float useableSpace = ( maxHeight - Hud_GetHeight( sliderPanel ) ) + + float jump = minYPos - ( useableSpace / ( float( file.filteredList.len() ) ) ) + + int pos = expect int( expect array( Hud_GetPos( sliderButton ) )[1] ) + float newPos = float( pos - mouseDeltaBuffer.deltaY ) + FlushMouseDeltaBuffer() + + if ( newPos < maxYPos ) newPos = maxYPos + if ( newPos > minYPos ) newPos = minYPos + + Hud_SetPos( sliderButton, 2, newPos ) + Hud_SetPos( sliderPanel, 2, newPos ) + Hud_SetPos( movementCapture, 2, newPos ) + + file.scrollOffset = -int( ( ( newPos - minYPos ) / useableSpace ) * ( file.filteredList.len() - BUTTONS_PER_PAGE ) ) + UpdateList() +} + +void function UpdateListSliderHeight() +{ + var sliderButton = Hud_GetChild( file.menu, "BtnModListSlider" ) + var sliderPanel = Hud_GetChild( file.menu, "BtnModListSliderPanel" ) + var movementCapture = Hud_GetChild( file.menu, "MouseMovementCapture" ) + + float mods = float ( file.filteredList.len() ) + + float maxHeight = 615.0 * ( GetScreenSize()[1] / 1080.0 ) // why the hardcoded 320/80??? + float minHeight = 80.0 * ( GetScreenSize()[1] / 1080.0 ) + + float height = maxHeight * ( float( BUTTONS_PER_PAGE ) / mods ) + + if ( height > maxHeight ) height = maxHeight + if ( height < minHeight ) height = minHeight + + Hud_SetHeight( sliderButton, height ) + Hud_SetHeight( sliderPanel, height ) + Hud_SetHeight( movementCapture, height ) +} + +void function UpdateList() +{ + Hud_SetFocused( Hud_GetChild( file.menu, "BtnModsSearch" ) ) + file.updatingList = true + + array filteredList = [] + + array filters = split( file.filterText, "," ) + array list = file.conVarList + if ( filters.len() <= 0 ) + filters.append( "" ) + foreach( string f in filters ) + { + string filter = strip( f ) + string lastCatNameInFilter = "" + string lastModNameInFilter = "" + int curCatIndex = 0 + int curModTitleIndex = -1 + for ( int i = 0; i < list.len(); i++ ) + { + ConVarData prev = list[ maxint( 0, i - 1 ) ] + ConVarData c = list[i] + ConVarData next = list[ minint( list.len() - 1, i + 1 ) ] + if ( c.isEmptySpace ) + continue + + string displayName = c.displayName + if ( c.isModName ) + { + displayName = c.modName + curModTitleIndex = i + } + if ( c.isCategoryName ) + { + displayName = c.catName + curCatIndex = i + } + if ( filter == "" || SanitizeDisplayName( Localize( displayName ) ).tolower().find( filter.tolower() ) != null ) + { + if ( c.isModName ) + { + lastModNameInFilter = c.modName + array modVars = GetAllVarsInMod( list, c.modName ) + if ( filteredList.len() <= 0 && modVars[0].spaceType == eEmptySpaceType.None ) + filteredList.extend( modVars.slice( 1, modVars.len() ) ) + else filteredList.extend( modVars ) + + i += modVars.len() - 1 + } + else if ( c.isCategoryName ) + { + if ( lastModNameInFilter != c.modName ) + { + array modVars = GetModConVarDatas( list, curModTitleIndex ) + if ( filteredList.len() <= 0 && modVars[0].spaceType == eEmptySpaceType.None ) + filteredList.extend( modVars.slice( 1, modVars.len() ) ) + else filteredList.extend( modVars ) + + lastModNameInFilter = c.modName + } + filteredList.extend( GetAllVarsInCategory( list, c.catName ) ) + i += GetAllVarsInCategory( list, c.catName ).len() - 1 + lastCatNameInFilter = c.catName + } + else + { + if ( lastModNameInFilter != c.modName ) + { + array modVars = GetModConVarDatas( list, curModTitleIndex ) + if ( filteredList.len() <= 0 && modVars[0].spaceType == eEmptySpaceType.None ) + filteredList.extend( modVars.slice( 1, modVars.len() ) ) + else filteredList.extend( modVars ) + + lastModNameInFilter = c.modName + } + if ( lastCatNameInFilter != c.catName ) + { + filteredList.extend( GetCatConVarDatas( curCatIndex ) ) + lastCatNameInFilter = c.catName + } + filteredList.append( c ) + } + } + } + list = filteredList + filteredList = [] + } + filteredList = list + + + file.filteredList = filteredList + + int j = int( min( file.filteredList.len() + file.scrollOffset, BUTTONS_PER_PAGE ) ) + + for ( int i = 0; i < BUTTONS_PER_PAGE; i++ ) + { + Hud_SetEnabled( file.modPanels[i], i < j ) + Hud_SetVisible( file.modPanels[i], i < j ) + + if ( i < j ) + SetModMenuNameText( file.modPanels[i] ) + } + file.updatingList = false + + if ( file.conVarList.len() <= 0 ) + { + Hud_SetVisible( Hud_GetChild( file.menu, "NoResultLabel" ), true ) + Hud_SetText( Hud_GetChild( file.menu, "NoResultLabel" ), "#NO_MODS" ) + } + else if ( file.filteredList.len() <= 0 ) + { + Hud_SetVisible( Hud_GetChild( file.menu, "NoResultLabel" ), true ) + Hud_SetText( Hud_GetChild( file.menu, "NoResultLabel" ), "#NO_RESULTS" ) + } + else + { + Hud_Hide( Hud_GetChild( file.menu, "NoResultLabel" ) ) + } +} + +array function GetModConVarDatas( array arr, int index ) +{ + if ( index <= 1 ) + return [ arr[ index - 1 ], arr[ index ], arr[ index + 1 ] ] + return [ arr[ index - 2 ], arr[ index - 1 ], arr[ index ], arr[ index + 1 ] ] +} + +array function GetCatConVarDatas( int index ) +{ + if ( file.conVarList[ index - 1 ].spaceType != eEmptySpaceType.None ) + return [ file.conVarList[ index ] ] + return [ file.conVarList[ index - 1 ], file.conVarList[ index ] ] +} + +array function GetAllVarsInCategory( array arr, string catName ) +{ + array vars = [] + for ( int i = 0; i < arr.len(); i++ ) + { + ConVarData c = arr[i] + if ( c.catName == catName ) + { + vars.append( arr[i] ) + } + } + return vars +} + +array function GetAllVarsInMod( array arr, string modName ) +{ + array vars = [] + for ( int i = 0; i < arr.len(); i++ ) + { + ConVarData c = arr[i] + if ( c.modName == modName ) + { + vars.append( arr[i] ) + } + } + return vars +} + +void function SetModMenuNameText( var button ) +{ + int index = int ( Hud_GetScriptID( button ) ) + file.scrollOffset + ConVarData conVar = file.filteredList[ int ( Hud_GetScriptID( button ) ) + file.scrollOffset ] + + var panel = file.modPanels[ int ( Hud_GetScriptID( button ) ) ] + + var label = Hud_GetChild( panel, "BtnMod" ) + var textField = Hud_GetChild( panel, "TextEntrySetting" ) + var enumButton = Hud_GetChild( panel, "EnumSelectButton" ) + var resetButton = Hud_GetChild( panel, "ResetModToDefault" ) + var resetVGUI = Hud_GetChild( panel, "ResetModImage" ) + var bottomLine = Hud_GetChild( panel, "BottomLine" ) + var topLine = Hud_GetChild( panel, "TopLine" ) + var modTitle = Hud_GetChild( panel, "ModTitle" ) + var customMenuButton = Hud_GetChild( panel, "OpenCustomMenu") + var slider = Hud_GetChild( panel, "Slider" ) + Hud_SetVisible( slider, false ) + Hud_SetEnabled( slider, true ) + + + if ( conVar.isEmptySpace ) + { + string s = "" + Hud_SetPos( label, 0, 0 ) + Hud_SetVisible( label, false ) + Hud_SetVisible( textField, false ) + Hud_SetVisible( enumButton, false ) + Hud_SetVisible( resetButton, false ) + Hud_SetVisible( resetVGUI, false ) + Hud_SetVisible( modTitle, false ) + Hud_SetVisible( customMenuButton, false ) + Hud_SetVisible( bottomLine, false ) + Hud_SetVisible( topLine, false ) + switch ( conVar.spaceType ) + { + case eEmptySpaceType.TopBar: + Hud_SetVisible( topLine, true ) + return + + case eEmptySpaceType.BottomBar: + Hud_SetVisible( bottomLine, true ) + return + + case eEmptySpaceType.None: + return + } + } + + Hud_SetVisible( textField, !conVar.isCategoryName ) + Hud_SetVisible( bottomLine, conVar.isCategoryName || conVar.spaceType == eEmptySpaceType.BottomBar ) + Hud_SetVisible( topLine, false ) + Hud_SetVisible( enumButton, !conVar.isCategoryName && conVar.isEnumSetting ) + Hud_SetVisible( modTitle, conVar.isModName ) + Hud_SetVisible( customMenuButton, false ) + float scaleX = GetScreenSize()[1] / 1080.0 + float scaleY = GetScreenSize()[1] / 1080.0 + if ( conVar.sliderEnabled ) + { + Hud_SetSize( slider, int( 320 * scaleX ), int( 45 * scaleY ) ) + MS_Slider s = file.sliders[ int ( Hud_GetScriptID( button ) ) ] + MS_Slider_SetMin( s, conVar.min ) + MS_Slider_SetMax( s, conVar.max ) + MS_Slider_SetStepSize( s, conVar.stepSize ) + MS_Slider_SetValue( s, GetConVarFloat( conVar.conVar ) ) + } + else Hud_SetSize( slider, 0, int( 45 * scaleY ) ) + if ( conVar.isCustomButton ) + { + Hud_SetVisible( label, false ) + Hud_SetVisible( textField, false ) + Hud_SetVisible( enumButton, false ) + Hud_SetVisible( resetButton, false ) + Hud_SetVisible( modTitle, false ) + Hud_SetVisible( resetVGUI, false ) + Hud_SetVisible( customMenuButton, true ) + Hud_SetText( customMenuButton, conVar.displayName ) + } + else if ( conVar.isModName ) + { + Hud_SetText( modTitle, conVar.modName ) + Hud_SetPos( label, 0, 0 ) + Hud_SetVisible( label, false ) + Hud_SetVisible( textField, false ) + Hud_SetVisible( enumButton, false ) + Hud_SetVisible( resetButton, false ) + Hud_SetVisible( resetVGUI, false ) + Hud_SetVisible( bottomLine, false ) + Hud_SetVisible( topLine, false ) + } + else if ( conVar.isCategoryName ) + { + Hud_SetText( label, conVar.catName ) + Hud_SetPos( label, 0, 0 ) + Hud_SetSize( label, int( scaleX * ( 1180 - 420 - 85 ) ), int( scaleY * 40 ) ) + Hud_SetVisible( label, true ) + Hud_SetVisible( textField, false ) + Hud_SetVisible( enumButton, false ) + Hud_SetVisible( resetButton, true ) + Hud_SetVisible( resetVGUI, true ) + + Hud_SetSize( resetButton, int( scaleX * 90 ), int( scaleY * 40 ) ) + } + else { + Hud_SetVisible( slider, conVar.sliderEnabled ) + + Hud_SetText( label, conVar.displayName ) + if (conVar.type == "float") + Hud_SetText( textField, string( GetConVarFloat(conVar.conVar) ) ) + else Hud_SetText( textField, conVar.isEnumSetting ? conVar.values[ GetConVarInt( conVar.conVar ) ] : GetConVarString( conVar.conVar ) ) + Hud_SetPos( label, int(scaleX * 25), 0 ) + Hud_SetText( resetButton, "" ) + if (conVar.sliderEnabled) + Hud_SetSize( label, int(scaleX * (375 + 85)), int(scaleY * 40) ) + else Hud_SetSize( label, int(scaleX * (375 + 405)), int(scaleY * 40) ) + if ( conVar.type == "float" ) + Hud_SetText( textField, string( GetConVarFloat( conVar.conVar ) ) ) + else Hud_SetText( textField, conVar.isEnumSetting ? conVar.values[ GetConVarInt( conVar.conVar ) ] : GetConVarString( conVar.conVar ) ) + Hud_SetPos( label, int( scaleX * 25 ), 0 ) + Hud_SetText( resetButton, "" ) + Hud_SetSize( resetButton, int( scaleX * 90 ), int( scaleY * 40 ) ) + if ( conVar.sliderEnabled ) + Hud_SetSize( label, int( scaleX * ( 375 + 85 ) ), int( scaleY * 40 ) ) + else Hud_SetSize( label, int( scaleX * ( 375 + 405 ) ), int( scaleY * 40 ) ) + Hud_SetVisible( label, true ) + Hud_SetVisible( textField, true ) + Hud_SetVisible( resetButton, true ) + Hud_SetVisible( resetVGUI, true ) + } +} + +void function CustomButtonPressed( var button ) +{ + var panel = Hud_GetParent( button ) + ConVarData c = file.filteredList[ int( Hud_GetScriptID( panel ) ) + file.scrollOffset ] + c.onPress() +} + +void function OnScrollDown( var button ) +{ + if ( file.filteredList.len() <= BUTTONS_PER_PAGE ) return + file.scrollOffset += 5 + if ( file.scrollOffset + BUTTONS_PER_PAGE > file.filteredList.len() ) + { + file.scrollOffset = file.filteredList.len() - BUTTONS_PER_PAGE + } + UpdateList() + UpdateListSliderPosition() +} + +void function OnScrollUp( var button ) +{ + file.scrollOffset -= 5 + if ( file.scrollOffset < 0 ) + { + file.scrollOffset = 0 + } + UpdateList() + UpdateListSliderPosition() +} + +void function UpdateListSliderPosition() +{ + var sliderButton = Hud_GetChild( file.menu , "BtnModListSlider" ) + var sliderPanel = Hud_GetChild( file.menu , "BtnModListSliderPanel" ) + var movementCapture = Hud_GetChild( file.menu , "MouseMovementCapture" ) + + float mods = float ( file.filteredList.len() ) + + float minYPos = -40.0 * ( GetScreenSize()[1] / 1080.0 ) + float useableSpace = ( 615.0 * ( GetScreenSize()[1] / 1080.0 ) - Hud_GetHeight( sliderPanel ) ) + + float jump = minYPos - ( useableSpace / ( mods - float( BUTTONS_PER_PAGE ) ) * file.scrollOffset ) + + + if ( jump > minYPos ) jump = minYPos + + Hud_SetPos( sliderButton , 2, jump ) + Hud_SetPos( sliderPanel , 2, jump ) + Hud_SetPos( movementCapture , 2, jump ) +} + +void function OnModMenuOpened() +{ + if( !file.isOpen ) + { + file.scrollOffset = 0 + file.filterText = "" + + RegisterButtonPressedCallback( MOUSE_WHEEL_UP , OnScrollUp ) + RegisterButtonPressedCallback( MOUSE_WHEEL_DOWN , OnScrollDown ) + RegisterButtonPressedCallback( MOUSE_LEFT , OnClick ) + + OnFiltersChange() + file.isOpen = true + } +} + +void function OnClick( var button ) +{ + if (file.resetModButtons.contains(GetFocus())) + thread CheckFocus(GetFocus()) + if (GetFocus() == Hud_GetChild(file.menu, "NoResultLabel")) + thread CheckFocus(GetFocus()) +} + +void function CheckFocus( var button ) +{ + wait 0.05 + if (file.resetModButtons.contains(GetFocus())) + { + thread ResetConVar(GetFocus()) + } + if (GetFocus() == Hud_GetChild(file.menu, "NoResultLabel")) + LaunchExternalWebBrowser( "https://northstar.thunderstore.io/", WEBBROWSER_FLAG_FORCEEXTERNAL ) +} + +void function OnFiltersChange() +{ + file.scrollOffset = 0 + + UpdateList() + + UpdateListSliderHeight() +} + +void function OnModMenuClosed() +{ + DeregisterButtonPressedCallback( MOUSE_WHEEL_UP , OnScrollUp ) + DeregisterButtonPressedCallback( MOUSE_WHEEL_DOWN , OnScrollDown ) + DeregisterButtonPressedCallback( MOUSE_LEFT , OnClick ) + + file.scrollOffset = 0 + UpdateListSliderPosition() + file.isOpen = false +} + +void function ModSettings_AddModTitle( string modName, int stackPos = 2 ) +{ + file.currentMod = modName + if ( file.conVarList.len() > 0 ) + { + ConVarData catData + + catData.isEmptySpace = true + catData.modName = file.currentMod + + file.conVarList.append( catData ) + } + ConVarData topBar + topBar.isEmptySpace = true + topBar.modName = modName + topBar.spaceType = eEmptySpaceType.TopBar + + + ConVarData modData + + modData.modName = modName + modData.displayName = modName + modData.isModName = true + + + ConVarData botBar + botBar.isEmptySpace = true + botBar.modName = modName + botBar.spaceType = eEmptySpaceType.BottomBar + file.conVarList.extend( [ topBar, modData, botBar ] ) + file.setFuncs[ expect string( getstackinfos( stackPos )[ "func" ] ) ] <- false +} + +void function AddModTitle( string modName, int stackPos = 2 ) +{ + ModSettings_AddModTitle( modName, stackPos + 1 ) +} + +void function ModSettings_AddModCategory( string catName, int stackPos = 2 ) +{ + if ( !( getstackinfos( stackPos )[ "func" ] in file.setFuncs ) ) + throw getstackinfos( stackPos )[ "src" ] + " #" + getstackinfos( stackPos )[ "line" ] + "\nCannot add a category before a mod title!" + + ConVarData space + space.isEmptySpace = true + space.modName = file.currentMod + space.catName = catName + file.conVarList.append( space ) + + ConVarData catData + + catData.catName = catName + catData.displayName = catName + catData.modName = file.currentMod + catData.isCategoryName = true + + file.conVarList.append( catData ) + + file.currentCat = catName + file.setFuncs[ expect string( getstackinfos( stackPos )[ "func" ] ) ] = true +} + +void function AddModCategory( string catName, int stackPos = 2 ) +{ + ModSettings_AddModCategory( catName, stackPos + 1 ) +} + +void function ModSettings_AddButton( string buttonLabel, void functionref() onPress, int stackPos = 2 ) +{ + if ( !( getstackinfos( stackPos )[ "func" ] in file.setFuncs ) || !file.setFuncs[ expect string( getstackinfos( stackPos )[ "func" ] ) ] ) + throw getstackinfos( stackPos )[ "src" ] + " #" + getstackinfos( stackPos )[ "line" ] + "\nCannot add a button before a category and mod title!" + + ConVarData data + + data.isCustomButton = true + data.displayName = buttonLabel + data.modName = file.currentMod + data.catName = file.currentCat + data.onPress = onPress + + file.conVarList.append( data ) +} + +void function AddModSettingsButton( string buttonLabel, void functionref() onPress, int stackPos = 2 ) +{ + ModSettings_AddButton( buttonLabel, onPress, stackPos + 1 ) +} + +void function ModSettings_AddSetting( string conVar, string displayName, string type = "", int stackPos = 2 ) +{ + if ( !( getstackinfos( stackPos )[ "func" ] in file.setFuncs ) || !file.setFuncs[ expect string( getstackinfos( stackPos )[ "func" ] ) ] ) + throw getstackinfos( stackPos )[ "src" ] + " #" + getstackinfos( stackPos )[ "line" ] + "\nCannot add a setting before a category and mod title!" + ConVarData data + + data.catName = file.currentCat + data.conVar = conVar + data.modName = file.currentMod + data.displayName = displayName + data.type = type + + file.conVarList.append( data ) +} + +void function AddConVarSetting( string conVar, string displayName, string type = "", int stackPos = 2 ) +{ + ModSettings_AddSetting( conVar, displayName, type, stackPos + 1 ) +} + +void function ModSettings_AddSliderSetting( string conVar, string displayName, float min = 0.0, float max = 1.0, float stepSize = 0.1, bool forceClamp = false, int stackPos = 2 ) +{ + if ( !( getstackinfos( stackPos )[ "func" ] in file.setFuncs ) || !file.setFuncs[ expect string( getstackinfos( stackPos )[ "func" ] ) ] ) + throw getstackinfos( stackPos )[ "src" ] + " #" + getstackinfos( stackPos )[ "line" ] + "\nCannot add a setting before a category and mod title!" + ConVarData data + + data.catName = file.currentCat + data.conVar = conVar + data.modName = file.currentMod + data.displayName = displayName + data.type = "float" + data.sliderEnabled = true + data.forceClamp = false + data.min = min + data.max = max + data.stepSize = stepSize + + file.conVarList.append( data ) +} + +void function AddConVarSettingSlider( string conVar, string displayName, float min = 0.0, float max = 1.0, float stepSize = 0.1, bool forceClamp = false, int stackPos = 2 ) +{ + ModSettings_AddSliderSetting( conVar, displayName, min, max, stepSize, forceClamp, stackPos + 1 ) +} + +void function ModSettings_AddEnumSetting( string conVar, string displayName, array values, int stackPos = 2 ) +{ + if ( !( getstackinfos( stackPos )[ "func" ] in file.setFuncs ) || !file.setFuncs[ expect string( getstackinfos( stackPos )[ "func" ] ) ] ) + throw getstackinfos( stackPos )[ "src" ] + " #" + getstackinfos( stackPos )[ "line" ] + "\nCannot add a setting before a category and mod title!" + ConVarData data + + data.catName = file.currentCat + data.modName = file.currentMod + data.conVar = conVar + data.displayName = displayName + data.values = values + data.isEnumSetting = true + data.min = 0 + data.max = values.len() - 1.0 + data.sliderEnabled = values.len() > 2 + data.forceClamp = true + data.stepSize = 1 + + file.conVarList.append( data ) +} + +void function AddConVarSettingEnum( string conVar, string displayName, array values, int stackPos = 2 ) +{ + ModSettings_AddEnumSetting( conVar, displayName, values, stackPos + 1 ) +} + +void function OnSliderChange( var button ) +{ + if ( file.updatingList ) + return + var panel = Hud_GetParent( button ) + ConVarData c = file.filteredList[ int( Hud_GetScriptID( panel ) ) + file.scrollOffset ] + var textPanel = Hud_GetChild( panel, "TextEntrySetting" ) + + if ( c.isEnumSetting ) + { + int val = int( RoundToNearestInt( Hud_SliderControl_GetCurrentValue( button ) ) ) + SetConVarInt( c.conVar, val ) + Hud_SetText( textPanel, ( c.values[ GetConVarInt( c.conVar ) ] ) ) + MS_Slider_SetValue( file.sliders[ int( Hud_GetScriptID( Hud_GetParent( textPanel ) ) ) ], float( val ) ) + + return + } + float val = Hud_SliderControl_GetCurrentValue( button ) + if ( c.forceClamp ) + { + int mod = int( RoundToNearestInt( val % c.stepSize / c.stepSize ) ) + val = ( int( val / c.stepSize ) + mod ) * c.stepSize + } + SetConVarFloat( c.conVar, val ) + MS_Slider_SetValue( file.sliders[ int( Hud_GetScriptID( Hud_GetParent( textPanel ) ) ) ], val ) + + Hud_SetText( textPanel, string( GetConVarFloat( c.conVar ) ) ) +} + +void function SendTextPanelChanges( var textPanel ) +{ + ConVarData c = file.filteredList[ int( Hud_GetScriptID( Hud_GetParent( textPanel ) ) ) + file.scrollOffset ] + if ( c.conVar == "" ) return + // enums don't need to do this + if ( !c.isEnumSetting ) + { + string newSetting = Hud_GetUTF8Text( textPanel ) + + switch ( c.type ) + { + case "int": + try + { + SetConVarInt( c.conVar, newSetting.tointeger() ) + } + catch ( ex ) + { + ThrowInvalidValue( "This setting is an integer, and only accepts whole numbers." ) + Hud_SetText( textPanel, GetConVarString( c.conVar ) ) + } + case "bool": + if ( newSetting != "0" && newSetting != "1" ) + { + ThrowInvalidValue( "This setting is a boolean, and only accepts values of 0 or 1." ) + + // set back to previous value : ) + Hud_SetText( textPanel, string( GetConVarBool( c.conVar ) ) ) + + break + } + SetConVarBool( c.conVar, newSetting == "1" ) + break + case "float": + try + { + SetConVarFloat( c.conVar, newSetting.tofloat() ) + } + catch ( ex ) + { + printt( ex ) + ThrowInvalidValue( "This setting is a float, and only accepts a number - we could not parse this!\n\n( Use \".\" for the floating point, not \",\". )" ) + } + if ( c.sliderEnabled ) + { + var panel = Hud_GetParent( textPanel ) + MS_Slider s = file.sliders[ int ( Hud_GetScriptID( panel ) ) ] + + MS_Slider_SetValue( s, GetConVarFloat( c.conVar ) ) + } + break + case "float2": + try + { + array split = split( newSetting, " " ) + if ( split.len() != 2 ) + { + ThrowInvalidValue( "This setting is a float2, and only accepts a pair of numbers - you put in " + split.len() + "!" ) + Hud_SetText( textPanel, GetConVarString( c.conVar ) ) + break + } + vector settingTest = < split[0].tofloat(), split[1].tofloat(), 0 > + + SetConVarString( c.conVar, newSetting ) + } + catch ( ex ) + { + ThrowInvalidValue( "This setting is a float2, and only accepts a pair of numbers - you put something we could not parse!\n\n( Use \".\" for the floating point, not \",\". )" ) + Hud_SetText( textPanel, GetConVarString( c.conVar ) ) + } + break + // idk sometimes it's called Float3 most of the time it's called vector, I am not complaining. + case "vector": + case "float3": + try + { + array split = split( newSetting, " " ) + if ( split.len() != 3 ) + { + ThrowInvalidValue( "This setting is a float3, and only accepts a trio of numbers - you put in " + split.len() + "!" ) + Hud_SetText( textPanel, GetConVarString( c.conVar ) ) + break + } + vector settingTest = < split[0].tofloat(), split[1].tofloat(), 0 > + + SetConVarString( c.conVar, newSetting ) + } + catch ( ex ) + { + ThrowInvalidValue( "This setting is a float3, and only accepts a trio of numbers - you put something we could not parse!\n\n( Use \".\" for the floating point, not \",\". )" ) + Hud_SetText( textPanel, GetConVarString( c.conVar ) ) + } + break + default: + SetConVarString( c.conVar, newSetting ) + break; + } + } + else Hud_SetText( textPanel, Localize( c.values[ GetConVarInt( c.conVar ) ] ) ) +} + +void function ThrowInvalidValue( string desc ) +{ + DialogData dialogData + dialogData.header = "Invalid Value" + dialogData.image = $"ui/menu/common/dialog_error" + dialogData.message = desc + AddDialogButton( dialogData, "#OK" ) + OpenDialog( dialogData ) +} + +void function UpdateEnumSetting( var button ) +{ + int scriptId = int( Hud_GetScriptID( Hud_GetParent( button ) ) ) + ConVarData c = file.filteredList[ scriptId + file.scrollOffset ] + + var panel = file.modPanels[ scriptId ] + + var textPanel = Hud_GetChild( panel, "TextEntrySetting" ) + + string selectionVal = Hud_GetDialogListSelectionValue( button ) + + if ( selectionVal == "main" ) + return + + int enumVal = GetConVarInt( c.conVar ) + if ( selectionVal == "next" ) // enum val += 1 + enumVal = ( enumVal + 1 ) % c.values.len() + else // enum val -= 1 + { + enumVal-- + if ( enumVal == -1 ) + enumVal = c.values.len() - 1 + } + + SetConVarInt( c.conVar, enumVal ) + Hud_SetText( textPanel, c.values[ enumVal ] ) + + Hud_SetDialogListSelectionValue( button, "main" ) +} + +void function OnClearButtonPressed( var button ) +{ + file.filterText = "" + Hud_SetText( Hud_GetChild( file.menu, "BtnModsSearch" ), "" ) + + OnFiltersChange() +} + +string function SanitizeDisplayName( string displayName ) +{ + array parts = split( displayName, "^" ) + string result = "" + if ( parts.len() == 1 ) + return parts[0] + foreach ( string p in parts ) + { + if ( p == "" ) + { + result += "^" + continue + } + int i = 0 + for ( i = 0; i < 8 && i < p.len(); i++ ) + { + var c = p[i] + if ( ( c < 'a' || c > 'f' ) && ( c < 'A' || c > 'F' ) && ( c < '0' || c > '9' ) ) + break + } + if ( i == 0 ) + result += p + else result += p.slice( i, p.len() ) + } + return result +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut index 32a3c8f5e..52a99b6fe 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut @@ -52,7 +52,8 @@ void function UpdateVisibleModes() Hud_SetEnabled( buttons[ i ], true ) Hud_SetVisible( buttons[ i ], true ) - if ( !ModeSettings_RequiresAI( modesArray[ modeIndex ] ) || modesArray[ modeIndex ] == "aitdm" ) + // This check is refactored in the new mode menu so we can just ignore this atrocity + if ( !ModeSettings_RequiresAI( modesArray[ modeIndex ] ) || modesArray[ modeIndex ] == "aitdm" || modesArray[ modeIndex ] == "at" ) Hud_SetLocked( buttons[ i ], false ) else Hud_SetLocked( buttons[ i ], true ) diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_connect_password.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_connect_password.nut index b5a2e9b68..1e10aa45f 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_connect_password.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_connect_password.nut @@ -54,5 +54,8 @@ void function OnConnectWithPasswordMenuOpened() void function ConnectWithPassword( var button ) { if ( GetTopNonDialogMenu() == file.menu ) + { + TriggerConnectToServerCallbacks() thread ThreadedAuthAndConnectToServer( Hud_GetUTF8Text( Hud_GetChild( file.menu, "EnterPasswordBox" ) ) ) + } } \ No newline at end of file diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut index b663a25ac..c40461329 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut @@ -4,6 +4,9 @@ untyped global function AddNorthstarServerBrowserMenu global function ThreadedAuthAndConnectToServer +global function AddConnectToServerCallback +global function RemoveConnectToServerCallback +global function TriggerConnectToServerCallbacks // Stop peeking @@ -67,7 +70,6 @@ struct serverStruct { struct { // UI state vars var menu - int lastSelectedServer = 999 int focusedServerIndex = 0 int scrollOffset = 0 bool serverListRequestFailed = false @@ -79,6 +81,10 @@ struct { // filtered array of servers array serversArrayFiltered + + array filteredServers + ServerInfo& focusedServer + ServerInfo& lastSelectedServer // UI references array serverButtons @@ -88,6 +94,8 @@ struct { array serversMap array serversGamemode array serversRegion + + array< void functionref( ServerInfo ) > connectCallbacks } file @@ -253,7 +261,7 @@ void function FlushMouseDeltaBuffer() void function SliderBarUpdate() { - if ( file.serversArrayFiltered.len() <= BUTTONS_PER_PAGE ) + if ( file.filteredServers.len() <= BUTTONS_PER_PAGE ) { FlushMouseDeltaBuffer() return @@ -270,7 +278,7 @@ void function SliderBarUpdate() float maxYPos = minYPos - ( maxHeight - Hud_GetHeight( sliderPanel ) ) float useableSpace = ( maxHeight - Hud_GetHeight( sliderPanel ) ) - float jump = minYPos - ( useableSpace / ( float( file.serversArrayFiltered.len() ) ) ) + float jump = minYPos - ( useableSpace / ( float( file.filteredServers.len() ) ) ) // got local from official respaw scripts, without untyped throws an error local pos = Hud_GetPos( sliderButton )[1] @@ -284,7 +292,7 @@ void function SliderBarUpdate() Hud_SetPos( sliderPanel , 2, newPos ) Hud_SetPos( movementCapture , 2, newPos ) - file.scrollOffset = -int( ( ( newPos - minYPos ) / useableSpace ) * ( file.serversArrayFiltered.len() - BUTTONS_PER_PAGE ) ) + file.scrollOffset = -int( ( ( newPos - minYPos ) / useableSpace ) * ( file.filteredServers.len() - BUTTONS_PER_PAGE ) ) UpdateShownPage() } @@ -328,13 +336,13 @@ void function UpdateListSliderPosition( int servers ) void function OnScrollDown( var button ) { - if (file.serversArrayFiltered.len() <= BUTTONS_PER_PAGE) return + if (file.filteredServers.len() <= BUTTONS_PER_PAGE) return file.scrollOffset += 5 - if (file.scrollOffset + BUTTONS_PER_PAGE > file.serversArrayFiltered.len()) { - file.scrollOffset = file.serversArrayFiltered.len() - BUTTONS_PER_PAGE + if (file.scrollOffset + BUTTONS_PER_PAGE > file.filteredServers.len()) { + file.scrollOffset = file.filteredServers.len() - BUTTONS_PER_PAGE } UpdateShownPage() - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) } void function OnScrollUp( var button ) @@ -344,7 +352,7 @@ void function OnScrollUp( var button ) file.scrollOffset = 0 } UpdateShownPage() - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) } //////////////////////////// @@ -484,7 +492,7 @@ void function OnHitDummyTop( var button ) { // only update if list position changed UpdateShownPage() - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) DisplayFocusedServerInfo( file.serverButtonFocusedID ) Hud_SetFocused( Hud_GetChild( file.menu, "BtnServer1" ) ) } @@ -493,10 +501,10 @@ void function OnHitDummyTop( var button ) void function OnHitDummyBottom( var button ) { file.scrollOffset += 1 - if ( file.scrollOffset + BUTTONS_PER_PAGE > file.serversArrayFiltered.len() ) + if ( file.scrollOffset + BUTTONS_PER_PAGE > file.filteredServers.len() ) { // was at bottom already - file.scrollOffset = file.serversArrayFiltered.len() - BUTTONS_PER_PAGE + file.scrollOffset = file.filteredServers.len() - BUTTONS_PER_PAGE Hud_SetFocused( Hud_GetChild( file.menu, "BtnServerSearch" ) ) HideServerInfo() } @@ -504,7 +512,7 @@ void function OnHitDummyBottom( var button ) { // only update if list position changed UpdateShownPage() - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) DisplayFocusedServerInfo( file.serverButtonFocusedID ) Hud_SetFocused( Hud_GetChild( file.menu, "BtnServer15" ) ) } @@ -518,15 +526,15 @@ void function OnHitDummyAfterFilterClear( var button ) void function OnDownArrowSelected( var button ) { - if ( file.serversArrayFiltered.len() <= BUTTONS_PER_PAGE ) return + if ( file.filteredServers.len() <= BUTTONS_PER_PAGE ) return file.scrollOffset += 1 - if ( file.scrollOffset + BUTTONS_PER_PAGE > file.serversArrayFiltered.len() ) + if ( file.scrollOffset + BUTTONS_PER_PAGE > file.filteredServers.len() ) { - file.scrollOffset = file.serversArrayFiltered.len() - BUTTONS_PER_PAGE + file.scrollOffset = file.filteredServers.len() - BUTTONS_PER_PAGE } UpdateShownPage() - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) } @@ -539,7 +547,7 @@ void function OnUpArrowSelected( var button ) } UpdateShownPage() - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) } //////////////////////// @@ -642,7 +650,7 @@ void function FilterAndUpdateList( var n ) filterArguments.hideProtected = GetConVarBool( "filter_hide_protected" ) file.scrollOffset = 0 - UpdateListSliderPosition( file.serversArrayFiltered.len() ) + UpdateListSliderPosition( file.filteredServers.len() ) HideServerInfo() FilterServerList() @@ -741,51 +749,42 @@ void function WaitForServerListRequest() void function FilterServerList() { - file.serversArrayFiltered.clear() + file.filteredServers.clear() int totalPlayers = 0 - for ( int i = 0; i < NSGetServerCount(); i++ ) - { - serverStruct tempServer - tempServer.serverIndex = i - tempServer.serverProtected = NSServerRequiresPassword( i ) - tempServer.serverName = NSGetServerName( i ) - tempServer.serverPlayers = NSGetServerPlayerCount( i ) - tempServer.serverPlayersMax = NSGetServerMaxPlayerCount( i ) - tempServer.serverMap = NSGetServerMap( i ) - tempServer.serverGamemode = GetGameModeDisplayName( NSGetServerPlaylist ( i ) ) - tempServer.serverRegion = NSGetServerRegion( i ) - - totalPlayers += tempServer.serverPlayers + array servers = NSGetGameServers() + foreach ( ServerInfo server in servers ) + { + totalPlayers += server.playerCount // Filters - if ( filterArguments.hideEmpty && tempServer.serverPlayers == 0 ) + if ( filterArguments.hideEmpty && server.playerCount == 0 ) continue; - if ( filterArguments.hideFull && tempServer.serverPlayers == tempServer.serverPlayersMax ) + if ( filterArguments.hideFull && server.playerCount == server.maxPlayerCount ) continue; - if ( filterArguments.hideProtected && tempServer.serverProtected ) + if ( filterArguments.hideProtected && server.requiresPassword ) continue; - if ( filterArguments.filterMap != "SWITCH_ANY" && filterArguments.filterMap != tempServer.serverMap ) + if ( filterArguments.filterMap != "SWITCH_ANY" && filterArguments.filterMap != server.map ) continue; - if ( filterArguments.filterGamemode != "SWITCH_ANY" && filterArguments.filterGamemode != tempServer.serverGamemode ) + if ( filterArguments.filterGamemode != "SWITCH_ANY" && filterArguments.filterGamemode != GetGameModeDisplayName(server.playlist) ) continue; - + // Search if ( filterArguments.useSearch ) { array sName - sName.append( tempServer.serverName.tolower() ) - sName.append( Localize( GetMapDisplayName( tempServer.serverMap ) ).tolower() ) - sName.append( tempServer.serverMap.tolower() ) - sName.append( tempServer.serverGamemode.tolower() ) - sName.append( Localize( tempServer.serverGamemode ).tolower() ) - sName.append( NSGetServerDescription( i ).tolower() ) - sName.append( NSGetServerRegion( i ).tolower() ) + sName.append( server.name.tolower() ) + sName.append( Localize( GetMapDisplayName( server.map ) ).tolower() ) + sName.append( server.map.tolower() ) + sName.append( server.playlist.tolower() ) + sName.append( Localize( server.playlist ).tolower() ) + sName.append( server.description.tolower() ) + sName.append( server.region.tolower() ) string sTerm = filterArguments.searchTerm.tolower() @@ -799,9 +798,8 @@ void function FilterServerList() if ( !found ) continue; } - - // Server fits our requirements, add it to the list - file.serversArrayFiltered.append( tempServer ) + + file.filteredServers.append( server ) } // Update player and server count @@ -824,23 +822,22 @@ void function UpdateShownPage() Hud_SetText( file.serversRegion[ i ], "" ) } - int j = file.serversArrayFiltered.len() > BUTTONS_PER_PAGE ? BUTTONS_PER_PAGE : file.serversArrayFiltered.len() + int j = file.filteredServers.len() > BUTTONS_PER_PAGE ? BUTTONS_PER_PAGE : file.filteredServers.len() for ( int i = 0; i < j; i++ ) { - int buttonIndex = file.scrollOffset + i - int serverIndex = file.serversArrayFiltered[ buttonIndex ].serverIndex + ServerInfo server = file.filteredServers[ buttonIndex ] Hud_SetEnabled( file.serverButtons[ i ], true ) Hud_SetVisible( file.serverButtons[ i ], true ) - Hud_SetVisible( file.serversProtected[ i ], file.serversArrayFiltered[ buttonIndex ].serverProtected ) - Hud_SetText( file.serversName[ i ], file.serversArrayFiltered[ buttonIndex ].serverName ) - Hud_SetText( file.playerCountLabels[ i ], format( "%i/%i", file.serversArrayFiltered[ buttonIndex ].serverPlayers, file.serversArrayFiltered[ buttonIndex ].serverPlayersMax ) ) - Hud_SetText( file.serversMap[ i ], GetMapDisplayName( file.serversArrayFiltered[ buttonIndex ].serverMap ) ) - Hud_SetText( file.serversGamemode[ i ], file.serversArrayFiltered[ buttonIndex ].serverGamemode ) - Hud_SetText( file.serversRegion[ i ], file.serversArrayFiltered[ buttonIndex ].serverRegion ) + Hud_SetVisible( file.serversProtected[ i ], server.requiresPassword ) + Hud_SetText( file.serversName[ i ], server.name ) + Hud_SetText( file.playerCountLabels[ i ], format( "%i/%i", server.playerCount, server.maxPlayerCount ) ) + Hud_SetText( file.serversMap[ i ], GetMapDisplayName( server.map ) ) + Hud_SetText( file.serversGamemode[ i ], GetGameModeDisplayName( server.playlist ) ) + Hud_SetText( file.serversRegion[ i ], server.region ) } @@ -850,7 +847,7 @@ void function UpdateShownPage() Hud_SetVisible( file.serverButtons[ 0 ], true ) Hud_SetText( file.serversName[ 0 ], "#NS_SERVERBROWSER_NOSERVERS" ) } - UpdateListSliderHeight( float( file.serversArrayFiltered.len() ) ) + UpdateListSliderHeight( float( file.filteredServers.len() ) ) } void function OnServerButtonFocused( var button ) @@ -860,8 +857,9 @@ void function OnServerButtonFocused( var button ) int scriptID = int ( Hud_GetScriptID( button ) ) file.serverButtonFocusedID = scriptID - if ( file.serversArrayFiltered.len() > 0 ) - file.focusedServerIndex = file.serversArrayFiltered[ file.scrollOffset + scriptID ].serverIndex + if ( file.filteredServers.len() > 0 ) + // file.focusedServerIndex = file.filteredServers[ file.scrollOffset + scriptID ].serverIndex + file.focusedServer = file.filteredServers[ file.scrollOffset + scriptID ] DisplayFocusedServerInfo( scriptID ) } @@ -882,13 +880,12 @@ void function CheckDoubleClick( int scriptID, bool wasClickNav ) int serverIndex = file.scrollOffset + scriptID bool sameServer = false - if ( file.lastSelectedServer == serverIndex ) sameServer = true - + if ( file.lastSelectedServer == file.filteredServers[ serverIndex ] ) sameServer = true file.serverSelectedTimeLast = file.serverSelectedTime file.serverSelectedTime = Time() - file.lastSelectedServer = serverIndex + file.lastSelectedServer = file.filteredServers[ serverIndex ] if ( wasClickNav && ( file.serverSelectedTime - file.serverSelectedTimeLast < DOUBLE_CLICK_TIME_MS ) && sameServer ) { @@ -900,7 +897,7 @@ void function DisplayFocusedServerInfo( int scriptID ) { if ( scriptID == 999 || scriptID == -1 || scriptID == 16 ) return - if ( NSIsRequestingServerList() || NSGetServerCount() == 0 || file.serverListRequestFailed || file.serversArrayFiltered.len() == 0 ) + if ( NSIsRequestingServerList() || NSGetServerCount() == 0 || file.serverListRequestFailed || file.filteredServers.len() == 0 ) return var menu = GetMenu( "ServerBrowserMenu" ) @@ -908,6 +905,7 @@ void function DisplayFocusedServerInfo( int scriptID ) int serverIndex = file.scrollOffset + scriptID if ( serverIndex < 0 ) serverIndex = 0 + ServerInfo server = file.filteredServers[ serverIndex ] Hud_SetVisible( Hud_GetChild( menu, "BtnServerDescription" ), true ) Hud_SetVisible( Hud_GetChild( menu, "BtnServerMods" ), true ) @@ -915,39 +913,39 @@ void function DisplayFocusedServerInfo( int scriptID ) // text panels Hud_SetVisible( Hud_GetChild( menu, "LabelDescription" ), true ) Hud_SetVisible( Hud_GetChild( menu, "LabelMods" ), false ) - Hud_SetText( Hud_GetChild( menu, "LabelDescription" ), NSGetServerDescription( file.serversArrayFiltered[ serverIndex ].serverIndex ) + "\n\nRequired Mods:\n" + FillInServerModsLabel( file.serversArrayFiltered[ serverIndex ].serverIndex ) ) + Hud_SetText( Hud_GetChild( menu, "LabelDescription" ), server.description + "\n\nRequired Mods:\n" + FillInServerModsLabel( server.requiredMods ) ) // map name/image/server name - string map = file.serversArrayFiltered[ serverIndex ].serverMap + string map = server.map Hud_SetVisible( Hud_GetChild( menu, "NextMapImage" ), true ) Hud_SetVisible( Hud_GetChild( menu, "NextMapBack" ), true ) RuiSetImage( Hud_GetRui( Hud_GetChild( menu, "NextMapImage" ) ), "basicImage", GetMapImageForMapName( map ) ) Hud_SetVisible( Hud_GetChild( menu, "NextMapName" ), true ) Hud_SetText( Hud_GetChild( menu, "NextMapName" ), GetMapDisplayName( map ) ) Hud_SetVisible( Hud_GetChild( menu, "ServerName" ), true ) - Hud_SetText( Hud_GetChild( menu, "ServerName" ), NSGetServerName( file.serversArrayFiltered[ serverIndex ].serverIndex ) ) + Hud_SetText( Hud_GetChild( menu, "ServerName" ), server.name ) // mode name/image - string mode = file.serversArrayFiltered[ serverIndex ].serverGamemode + string mode = server.playlist Hud_SetVisible( Hud_GetChild( menu, "NextModeIcon" ), true ) RuiSetImage( Hud_GetRui( Hud_GetChild( menu, "NextModeIcon" ) ), "basicImage", GetPlaylistThumbnailImage( mode ) ) Hud_SetVisible( Hud_GetChild( menu, "NextGameModeName" ), true ) if ( mode.len() != 0 ) - Hud_SetText( Hud_GetChild( menu, "NextGameModeName" ), mode ) + Hud_SetText( Hud_GetChild( menu, "NextGameModeName" ), GetGameModeDisplayName( mode ) ) else Hud_SetText( Hud_GetChild( menu, "NextGameModeName" ), "#NS_SERVERBROWSER_UNKNOWNMODE" ) } -string function FillInServerModsLabel( int server ) +string function FillInServerModsLabel( array mods ) { string ret - for ( int i = 0; i < NSGetServerRequiredModsCount( server ); i++ ) + foreach ( RequiredModInfo mod in mods ) { - ret += " " - ret += NSGetServerRequiredModName( server, i ) + " v" + NSGetServerRequiredModVersion( server, i ) + "\n" + ret += format( " %s v%s\n", mod.name, mod.version ) } + return ret } @@ -957,18 +955,17 @@ void function OnServerSelected( var button ) if ( NSIsRequestingServerList() || NSGetServerCount() == 0 || file.serverListRequestFailed ) return - int serverIndex = file.focusedServerIndex + ServerInfo server = file.focusedServer - file.lastSelectedServer = serverIndex + file.lastSelectedServer = server - // check mods - for ( int i = 0; i < NSGetServerRequiredModsCount( serverIndex ); i++ ) + foreach ( RequiredModInfo mod in server.requiredMods ) { - if ( !NSGetModNames().contains( NSGetServerRequiredModName( serverIndex, i ) ) ) + if ( !NSGetModNames().contains( mod.name ) ) { DialogData dialogData dialogData.header = "#ERROR" - dialogData.message = "Missing mod \"" + NSGetServerRequiredModName( serverIndex, i ) + "\" v" + NSGetServerRequiredModVersion( serverIndex, i ) + dialogData.message = format( "Missing mod \"%s\" v%s", mod.name, mod.version ) dialogData.image = $"ui/menu/common/dialog_error" #if PC_PROG @@ -985,8 +982,8 @@ void function OnServerSelected( var button ) else { // this uses semver https://semver.org - array serverModVersion = split( NSGetServerRequiredModVersion( serverIndex, i ), "." ) - array clientModVersion = split( NSGetModVersionByModName( NSGetServerRequiredModName( serverIndex, i ) ), "." ) + array serverModVersion = split( mod.name, "." ) + array clientModVersion = split( NSGetModVersionByModName( mod.name ), "." ) bool semverFail = false // if server has invalid semver don't bother checking @@ -1004,7 +1001,7 @@ void function OnServerSelected( var button ) { DialogData dialogData dialogData.header = "#ERROR" - dialogData.message = "Server has mod \"" + NSGetServerRequiredModName( serverIndex, i ) + "\" v" + NSGetServerRequiredModVersion( serverIndex, i ) + " while we have v" + NSGetModVersionByModName( NSGetServerRequiredModName( serverIndex, i ) ) + dialogData.message = format( "Server has mod \"%s\" v%s while we have v%s", mod.name, mod.version, NSGetModVersionByModName( mod.name ) ) dialogData.image = $"ui/menu/common/dialog_error" #if PC_PROG @@ -1021,13 +1018,16 @@ void function OnServerSelected( var button ) } } - if ( NSServerRequiresPassword( serverIndex ) ) + if ( server.requiresPassword ) { OnCloseServerBrowserMenu() AdvanceMenu( GetMenu( "ConnectWithPasswordMenu" ) ) } else + { + TriggerConnectToServerCallbacks() thread ThreadedAuthAndConnectToServer() + } } @@ -1036,9 +1036,7 @@ void function ThreadedAuthAndConnectToServer( string password = "" ) if ( NSIsAuthenticatingWithServer() ) return - print( "trying to authenticate with server " + NSGetServerName( file.lastSelectedServer ) + " with password " + password ) - - NSTryAuthWithServer( file.lastSelectedServer, password ) + NSTryAuthWithServer( file.lastSelectedServer.index, password ) ToggleConnectingHUD( true ) @@ -1061,19 +1059,39 @@ void function ThreadedAuthAndConnectToServer( string password = "" ) if ( NSWasAuthSuccessful() ) { - bool modsChanged + bool modsChanged = false - array requiredMods - for ( int i = 0; i < NSGetServerRequiredModsCount( file.lastSelectedServer ); i++ ) - requiredMods.append( NSGetServerRequiredModName( file.lastSelectedServer, i ) ) + // disable all RequiredOnClient mods that are not required by the server and are currently enabled + foreach ( string modName in NSGetModNames() ) + { + if ( NSIsModRequiredOnClient( modName ) && NSIsModEnabled( modName ) ) + { + // find the mod name in the list of server required mods + bool found = false + foreach ( RequiredModInfo mod in file.lastSelectedServer.requiredMods ) + { + if (mod.name == modName) + { + found = true + break + } + } + // if we didnt find the mod name, disable the mod + if (!found) + { + modsChanged = true + NSSetModEnabled( modName, false ) + } + } + } - // unload mods we don't need, load necessary ones and reload mods before connecting - foreach ( string mod in NSGetModNames() ) + // enable all RequiredOnClient mods that are required by the server and are currently disabled + foreach ( RequiredModInfo mod in file.lastSelectedServer.requiredMods ) { - if ( NSIsModRequiredOnClient( mod ) ) + if ( NSIsModRequiredOnClient( mod.name ) && !NSIsModEnabled( mod.name )) { - modsChanged = modsChanged || NSIsModEnabled( mod ) != requiredMods.contains( mod ) - NSSetModEnabled( mod, requiredMods.contains( mod ) ) + modsChanged = true + NSSetModEnabled( mod.name, true ) } } @@ -1106,7 +1124,7 @@ void function ThreadedAuthAndConnectToServer( string password = "" ) ////////////////////////////////////// // Shadow realm ////////////////////////////////////// -int function ServerSortLogic ( serverStruct a, serverStruct b ) +int function ServerSortLogic ( ServerInfo a, ServerInfo b ) { var aTemp var bTemp @@ -1117,44 +1135,44 @@ int function ServerSortLogic ( serverStruct a, serverStruct b ) switch ( filterDirection.sortingBy ) { case sortingBy.DEFAULT: - aTemp = a.serverPlayers - bTemp = b.serverPlayers + aTemp = a.playerCount + bTemp = b.playerCount // `1000` is assumed to always be higher than `serverPlayersMax` - if (aTemp + 1 < a.serverPlayersMax) + if (aTemp + 1 < a.maxPlayerCount) aTemp = aTemp+2000 - if (bTemp + 1 < b.serverPlayersMax) + if (bTemp + 1 < b.maxPlayerCount) bTemp = bTemp+2000 - if (aTemp + 1 == a.serverPlayersMax) + if (aTemp + 1 == a.maxPlayerCount) aTemp = aTemp+1000 - if (bTemp + 1 == b.serverPlayersMax) + if (bTemp + 1 == b.maxPlayerCount) bTemp = bTemp+1000 direction = filterDirection.serverName break; case sortingBy.NAME: - aTemp = a.serverName.tolower() - bTemp = b.serverName.tolower() + aTemp = a.name.tolower() + bTemp = b.name.tolower() direction = filterDirection.serverName break; case sortingBy.PLAYERS: - aTemp = a.serverPlayers - bTemp = b.serverPlayers + aTemp = a.playerCount + bTemp = b.playerCount direction = filterDirection.serverPlayers break; case sortingBy.MAP: - aTemp = Localize( a.serverMap ).tolower() - bTemp = Localize( b.serverMap ).tolower() + aTemp = Localize( a.map ).tolower() + bTemp = Localize( b.map ).tolower() direction = filterDirection.serverMap break; case sortingBy.GAMEMODE: - aTemp = Localize( a.serverGamemode ).tolower() - bTemp = Localize( b.serverGamemode ).tolower() + aTemp = Localize( a.playlist ).tolower() + bTemp = Localize( b.playlist ).tolower() direction = filterDirection.serverGamemode break; case sortingBy.REGION: - aTemp = a.serverRegion - bTemp = b.serverRegion + aTemp = a.region + bTemp = b.region direction = filterDirection.serverRegion break; default: @@ -1176,7 +1194,7 @@ void function SortServerListByDefault_Activate ( var button ) { filterDirection.sortingBy = sortingBy.DEFAULT - file.serversArrayFiltered.sort( ServerSortLogic ) + file.filteredServers.sort( ServerSortLogic ) filterDirection.serverName = !filterDirection.serverName @@ -1188,7 +1206,7 @@ void function SortServerListByName_Activate ( var button ) { filterDirection.sortingBy = sortingBy.NAME - file.serversArrayFiltered.sort( ServerSortLogic ) + file.filteredServers.sort( ServerSortLogic ) filterDirection.serverName = !filterDirection.serverName @@ -1200,7 +1218,7 @@ void function SortServerListByPlayers_Activate( var button ) { filterDirection.sortingBy = sortingBy.PLAYERS - file.serversArrayFiltered.sort( ServerSortLogic ) + file.filteredServers.sort( ServerSortLogic ) filterDirection.serverPlayers = !filterDirection.serverPlayers @@ -1211,7 +1229,7 @@ void function SortServerListByMap_Activate( var button ) { filterDirection.sortingBy = sortingBy.MAP - file.serversArrayFiltered.sort( ServerSortLogic ) + file.filteredServers.sort( ServerSortLogic ) filterDirection.serverMap = !filterDirection.serverMap @@ -1222,7 +1240,7 @@ void function SortServerListByGamemode_Activate( var button ) { filterDirection.sortingBy = sortingBy.GAMEMODE - file.serversArrayFiltered.sort( ServerSortLogic ) + file.filteredServers.sort( ServerSortLogic ) filterDirection.serverGamemode = !filterDirection.serverGamemode @@ -1233,9 +1251,33 @@ void function SortServerListByRegion_Activate( var button ) { filterDirection.sortingBy = sortingBy.REGION - file.serversArrayFiltered.sort( ServerSortLogic ) + file.filteredServers.sort( ServerSortLogic ) filterDirection.serverRegion = !filterDirection.serverRegion UpdateShownPage() } + +////////////////////////////////////// +// Callbacks +////////////////////////////////////// + +void function AddConnectToServerCallback( void functionref( ServerInfo ) callback ) +{ + if ( file.connectCallbacks.find( callback ) >= 0 ) + throw "ConnectToServerCallback has been registered twice. Duplicate callbacks are not allowed." + file.connectCallbacks.append( callback ) +} + +void function RemoveConnectToServerCallback( void functionref( ServerInfo ) callback ) +{ + file.connectCallbacks.fastremovebyvalue( callback ) +} + +void function TriggerConnectToServerCallbacks() +{ + foreach( callback in file.connectCallbacks ) + { + callback( file.lastSelectedServer ) + } +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/ns_slider.nut b/Northstar.Client/mod/scripts/vscripts/ui/ns_slider.nut new file mode 100644 index 000000000..33a79cdc8 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/ui/ns_slider.nut @@ -0,0 +1,52 @@ +// ModSettings_Slider +// since we are missing some utility functions (e.g. GetMax, GetMin, SetValue), this is basically a collection of workarounds. +global struct MS_Slider +{ + var slider + float min = 0.0 + float max = 1.0 + float stepSize = 0.05 +} + +globalize_all_functions + +MS_Slider function MS_Slider_Setup( var slider, float min = 0.0, float max = 1.0, float startVal = 0.0, float stepSize = 0.05 ) +{ + MS_Slider result + result.slider = slider + result.min = min + result.max = max + result.stepSize = stepSize + Hud_SliderControl_SetMin( slider, startVal ) + Hud_SliderControl_SetMax( slider, startVal ) + Hud_SliderControl_SetStepSize( slider, stepSize ) + Hud_SliderControl_SetMin( slider, min ) + Hud_SliderControl_SetMax( slider, max ) + return result +} + +void function MS_Slider_SetValue( MS_Slider slider, float val ) +{ + Hud_SliderControl_SetMin( slider.slider, val ) + Hud_SliderControl_SetMax( slider.slider, val ) + Hud_SliderControl_SetMin( slider.slider, slider.min ) + Hud_SliderControl_SetMax( slider.slider, slider.max ) +} + +void function MS_Slider_SetMin( MS_Slider slider, float min ) +{ + slider.min = min + Hud_SliderControl_SetMin( slider.slider, min ) +} + +void function MS_Slider_SetMax( MS_Slider slider, float max ) +{ + slider.max = max + Hud_SliderControl_SetMax( slider.slider, max ) +} + +void function MS_Slider_SetStepSize( MS_Slider slider, float stepSize ) +{ + slider.stepSize = stepSize + Hud_SliderControl_SetStepSize( slider.slider, stepSize ) +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut b/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut index 53d85387f..eef19b5ee 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut @@ -101,6 +101,10 @@ void function InitMainMenuPanel() var videoButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#VIDEO" ) Hud_AddEventHandler( videoButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "VideoMenu" ) ) ) #endif + + // MOD SETTINGS + var modSettingsButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#MOD_SETTINGS" ) + Hud_AddEventHandler( modSettingsButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "ModSettings" ) ) ) var spotlightLargeButton = Hud_GetChild( file.spotlightPanel, "SpotlightLarge" ) spotlightLargeButton.SetNavLeft( file.spButtons[0] ) diff --git a/Northstar.Custom/mod.json b/Northstar.Custom/mod.json index 66d29caee..f3f73fde3 100644 --- a/Northstar.Custom/mod.json +++ b/Northstar.Custom/mod.json @@ -1,7 +1,7 @@ { "Name": "Northstar.Custom", "Description": "Custom content for Northstar: extra weapons, gamemodes, etc.", - "Version": "1.13.0", + "Version": "1.14.0", "LoadPriority": 1, "RequiredOnClient": true, "ConVars": [ diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut index b025ff0a1..850aa7b37 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut @@ -117,8 +117,8 @@ void function GamemodeFW_Init() // _battery_port.gnut needs this RegisterSignal( "BatteryActivate" ) - AiGameModes_SetGruntWeapons( [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) AddCallback_EntitiesDidLoad( LoadEntities ) AddCallback_GameStateEnter( eGameState.Prematch, OnFWGamePrematch ) @@ -1235,10 +1235,18 @@ array function FW_GetTitanSpawnPointsForTeam( int team ) return validSpawnPoints } +// some maps have reversed startpoints! we need a hack +const array TITAN_POINT_REVERSED_MAPS = +[ + "mp_grave" +] + // "Respawn as Titan" don't follow the rateSpawnPoints, fix it manually entity function FW_ForcedTitanStartPoint( entity player, entity basePoint ) { int team = player.GetTeam() + if ( TITAN_POINT_REVERSED_MAPS.contains( GetMapName() ) ) + team = GetOtherTeam( player.GetTeam() ) array startPoints = SpawnPoints_GetTitanStart( team ) entity validPoint = startPoints[ RandomInt( startPoints.len() ) ] // choose a random( maybe not safe ) start point return validPoint diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut index d999bb4c5..be93193df 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut @@ -69,6 +69,7 @@ void function CreateGamemodeFW() GameMode_AddClientInit( FORT_WAR, CLGamemodeFW_Init ) #endif #if !UI + GameMode_SetScoreCompareFunc( FORT_WAR, CompareAssaultScore ) GameMode_AddSharedInit( FORT_WAR, SHGamemodeFW_Init ) #endif } diff --git a/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut new file mode 100644 index 000000000..95ab39158 --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut @@ -0,0 +1,1226 @@ +untyped + +global function MeleeShared_Init + +global function CodeCallback_OnMeleePressed +//global function CodeCallback_OnMeleeHeld +//global function CodeCallback_OnMeleeReleased +global function CodeCallback_IsValidMeleeExecutionTarget +global function CodeCallback_IsValidMeleeAttackTarget +global function CodeCallback_OnMeleeAttackAnimEvent +global function AddSyncedMeleeServerCallback +global function AddSyncedMeleeServerThink + +global function GetSyncedMeleeChooser +global function CreateSyncedMeleeChooser +global function PlayerTriesSyncedMelee +global function FindBestSyncedMelee +global function GetSyncedMeleeChooserForPlayerVsTarget +global function AddSyncedMelee +global function GetEyeOrigin +global function SetObjectCanBeMeleed +global function ObjectCanBeMeleed +global function ShouldClampTargetVelocity +global function ClampVerticalVelocity +global function IsInExecutionMeleeState + +global function GetLungeTargetForPlayer +global function Melee_IsAllowed +global function IsAttackerRef +global function AddCallback_IsValidMeleeExecutionTarget + +#if SERVER + global function Melee_Enable + global function Melee_Disable + global function SyncedMelee_Enable + global function SyncedMelee_Disable + global function InitMeleeAnimEventCallbacks + global function GetRefAnglesBetweenEnts + global function CreateMeleeScriptMoverBetweenEnts + global function ShouldHolsterWeaponForMelee + global function ShouldHolsterWeaponForSyncedMelee + global function NPCTriesSyncedMeleeVsPlayer +#endif + +const SMOOTH_TIME = 0.2 +const INSTA_KILL_TIME_THRESHOLD = 0.35 +const BUG_REPRO_MOVEMELEE = 19114 + +global struct SyncedMelee +{ + string ref + bool enabled = true + vector direction = < 1, 0, 0 > + float distance + float distanceSqr + string attackerAnimation1p + string attackerAnimation3p +// void function AddAnimEvent( entity ent, string eventName, void functionref( entity ent ) func, var optionalVar = null ) + array attacker3pAnimEvents + array target3pAnimEvents + string targetAnimation1p + string targetAnimation3p + string thirdPersonCameraAttachment + asset attachModel1p + string attachTag1p + float minDot = -1.0 // always happens + string animRefPos = "target" + bool canTargetNPCs = true + float percentDamageDealtPerHit = 1.0 + bool usableByPlayers = true + + float targetMinHealthRatio = 0.0 // target health ratio must be at least this high + float targetMaxHealthRatio = 1.0 // target health ratio must be at or below this + bool onlyIfLethal // only if the strike would be lethal + bool isAttackerRef = true + +} + +global struct SyncedMeleeChooser +{ + vector functionref( entity ) attackerOriginFunc + vector functionref( entity ) targetOriginFunc + array syncedMelees + bool displayMeleePrompt = true +} + +struct +{ + table > syncedMeleeChoosers + table > syncedMeleeServerCallbacks + table syncedMeleeServerThink + array isValidMeleeExecutionTargetCallBacks + string lastExecutionUsed = "" +} file + +function MeleeShared_Init() +{ + FlagInit( "ForceSyncedMelee" ) + + level.HUMAN_VS_TITAN_MELEE <- 1 + level.titan_attack_anim_event_count <- 0 + level.titan_attack_push_button_count <- 0 + + MeleeHumanShared_Init() + MeleeTitanShared_Init() + MeleeSyncedHumanShared_Init() + MeleeSyncedTitanShared_Init() + + #if SERVER + VerifySyncedMelee() + MeleeSyncedServer_Init() + #endif + + RegisterSignal( "SyncedMeleeComplete" ) + RegisterSignal( "OnSyncedMelee" ) + RegisterSignal( "OnSyncedMeleeVictim" ) + RegisterSignal( "OnSyncedMeleeAttacker" ) +} + +int function GetPlayerMeleeDamage( entity player ) +{ + Assert( player.IsPlayer() ) + foreach ( weapon in player.GetMainWeapons() ) + { + switch ( weapon.GetWeaponInfoFileKeyField( "fire_mode" ) ) + { + case "offhand_melee": + return expect int( weapon.GetWeaponInfoFileKeyField( "melee_damage" ) ) + } + } + + return 0 +} + +void function AddSyncedMeleeServerCallback( SyncedMeleeChooser chooser, void functionref( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target ) func ) +{ + if ( !( chooser in file.syncedMeleeServerCallbacks ) ) + file.syncedMeleeServerCallbacks[ chooser ] <- [] + + file.syncedMeleeServerCallbacks[ chooser ].append( func ) +} + +void function AddSyncedMeleeServerThink( SyncedMeleeChooser chooser, bool functionref( SyncedMelee action, entity player, entity target ) func ) +{ + file.syncedMeleeServerThink[ chooser ] <- func +} + + +function VerifySyncedMelee() +{ + foreach ( attackerChoosers in file.syncedMeleeChoosers ) + { + foreach ( chooser in attackerChoosers ) + { + //Assert( chooser in file.syncedMeleeServerCallbacks, "Need to add synced server melee callback for synced melee chooser" ) + //Assert( file.syncedMeleeServerCallbacks[ chooser ].len() > 0, "Need to create a callback for chooser" ) + Assert( chooser in file.syncedMeleeServerThink, "Need to add synced server melee callback for synced melee chooser" ) + } + } +} + +SyncedMeleeChooser function GetSyncedMeleeChooser( string attackerType, string victimType ) +{ + return file.syncedMeleeChoosers[ attackerType ][ victimType ] +} + +SyncedMeleeChooser function CreateSyncedMeleeChooser( string attackerType, string victimType ) +{ + SyncedMeleeChooser chooser + + chooser.attackerOriginFunc = GetEyeOrigin + chooser.targetOriginFunc = GetEyeOrigin + + if ( !( attackerType in file.syncedMeleeChoosers ) ) + file.syncedMeleeChoosers[ attackerType ] <- {} + + Assert( !( victimType in file.syncedMeleeChoosers[ attackerType ] ), "Already has " + victimType ) + file.syncedMeleeChoosers[ attackerType ][ victimType ] <- chooser + return chooser +} + +vector function GetEyeOrigin( entity ent ) +{ + return ent.EyePosition() +} + +void function AddCallback_IsValidMeleeExecutionTarget( bool functionref( entity attacker, entity target ) callbackFunc ) +{ + file.isValidMeleeExecutionTargetCallBacks.append( callbackFunc ) +} + +//Called after pressing the melee button to recheck for targets +bool function CodeCallback_IsValidMeleeExecutionTarget( entity attacker, entity target ) +{ + if ( attacker == target ) + return false + + if ( !ShouldPlayerExecuteTarget( attacker, target ) ) + return false + + if ( !attacker.IsOnGround() && attacker.IsHuman() ) + return false + + if ( !IsAlive( target ) ) + return false + + if ( target.IsInvulnerable() ) + return false + + if ( !CanBeMeleed( target ) ) + return false + + if ( target.IsNPC() && !target.CanBeMeleeExecuted() ) + return false + + // Disallow executing someone that is already in execution. That road leads to script errors and asserts. + if ( target.ContextAction_IsMeleeExecution() ) + return false + + if ( attacker.IsTitan() && target.IsTitan() ) + { + // no melee execute for berserker + if ( PlayerHasPassive( attacker, ePassives.PAS_BERSERKER ) ) + return false + + if ( PlayerHasPassive( attacker, ePassives.PAS_SHIFT_CORE ) ) + return false + + if ( HasSoul( target ) && target.GetTitanSoul().IsEjecting() ) + return false + + if ( attacker.ContextAction_IsActive() ) + return false + + if ( target.ContextAction_IsActive() ) + return false + + if ( GetCurrentPlaylistVarInt( "vortex_blocks_melee", 0 ) == 1 ) + { + vector traceStartPos = attacker.EyePosition() + vector traceEndPos = target.EyePosition() + VortexBulletHit ornull vortexHit = VortexBulletHitCheck( attacker, traceStartPos, traceEndPos ) + if ( vortexHit != null ) + { + return false + } + } + } + + if ( !CheckVerticallyCloseEnough( attacker, target ) ) + return false + + //No necksnaps while wall running or mantling + if ( attacker.IsWallRunning() ) + return false + + if ( attacker.IsTraversing() ) + return false + + if ( target.IsPlayer() ) //Disallow execution on a bunch of player-only actions + { + + if ( target.IsHuman() ) + { + if ( target.IsWallRunning() ) + return false + + if ( target.IsTraversing() ) + return false + + if ( !target.IsOnGround() ) //disallow mid-air necksnaps. Can't really do that for Titan executions since dash puts them in mid air... will have visual glitches unfortunately. + return false + + if ( target.IsCrouched() ) + return false + + if ( Rodeo_IsAttached( target ) ) + return false + } + } + + if ( target.IsPhaseShifted() ) + return false + + //Disallow executions on contextActions marked Busy. Note that this allows + //execution on melee and leeching context actions! + if ( target.ContextAction_IsBusy() ) + return false + + if ( target.IsNPC() ) //NPC only checks + { + if ( target.ContextAction_IsActive() ) + return false + + if ( !target.IsInterruptable() ) + return false + } + + if ( attacker.GetTeam() == target.GetTeam() ) + return false + +#if SERVER + if ( "syncedMeleeAttacker" in target.s ) //Don't allow necksnap on a guy who'se already getting necksnapped + return false +#endif // #if SERVER + + SyncedMeleeChooser ornull actions = GetSyncedMeleeChooserForPlayerVsTarget( attacker, target ) + if ( actions == null ) + return false + expect SyncedMeleeChooser( actions ) + + SyncedMelee ornull action = FindBestSyncedMelee( attacker, target, actions ) + if ( action == null ) + return false + + if ( !PlayerMelee_IsExecutionReachable( attacker, target, 0.3 ) ) + return false + + foreach ( callbackFunc in file.isValidMeleeExecutionTargetCallBacks ) + { + if ( !callbackFunc( attacker, target ) ) + { + return false + } + } + + return true +} + +bool function CodeCallback_IsValidMeleeAttackTarget( entity attacker, entity target ) +{ + if ( attacker == target ) + return false + + if ( target.IsBreakableGlass() ) + return true + + if ( !CanBeMeleed( target ) ) + return false + + if ( attacker.GetTeam() == target.GetTeam() ) + return false + +#if SERVER + if ( target.IsPlayer() ) + { + //Make titans not able to melee the pilot who is doing the embark animation + if ( GetTitanBeingRodeoed( target ) == attacker ) + return false + } +#endif // #if SERVER + + if ( target.IsPhaseShifted() ) + return false + + if ( target.GetParent() == attacker ) + return false + + #if SERVER //Awkward, needed because it's CBaseCombatCharacter on server and C_BaseCombatCharacter on client, and because we allow melee on non BaseCombatCharacters like props that don't have ContextActions defined + if ( target instanceof CBaseCombatCharacter && target.ContextAction_IsMeleeExecutionTarget() ) //Don't lunge towards a victim that is already being executed ) + return false + #elseif CLIENT + if ( target instanceof C_BaseCombatCharacter && target.ContextAction_IsMeleeExecutionTarget() ) //Don't lunge towards a victim that is already being executed ) + return false + #endif + + entity meleeWeapon = attacker.GetMeleeWeapon() + if ( !IsValid( meleeWeapon ) ) + return false; + + if ( !meleeWeapon.GetMeleeCanHitHumanSized() && IsHumanSized( target ) ) + return false; + if ( !meleeWeapon.GetMeleeCanHitTitans() && target.IsTitan() ) + return false; + + return true +} + +void function CodeCallback_OnMeleePressed( entity player ) +{ +#if SERVER + print( "SERVER: " + player + " pressed melee\n" ) +#else + print( "CLIENT: " + player + " pressed melee\n" ) +#endif + + if ( !Melee_IsAllowed( player ) ) + { +#if SERVER + print( "SERVER: Melee_IsAllowed() for " + player + " is false\n" ) +#else + print( "CLIENT: Melee_IsAllowed() for " + player + " is false\n" ) +#endif + return + } + +#if SERVER + if ( svGlobal.cloakBreaksOnMelee && IsCloaked( player ) ) + player.SetCloakFlicker( 1.0, 2.0 ) +#endif // #if SERVER + + if ( player.IsWeaponDisabled() ) + { +#if SERVER + print( "SERVER: IsWeaponDisabled() for " + player + " is true\n" ) +#else + print( "CLIENT: IsWeaponDisabled() for " + player + " is true\n" ) +#endif + return + } + + if ( player.PlayerMelee_GetState() != PLAYER_MELEE_STATE_NONE ) + { +#if SERVER + print( "SERVER: PlayerMelee_GetState() for " + player + " is " + player.PlayerMelee_GetState() + "\n" ) +#else + print( "CLIENT: PlayerMelee_GetState() for " + player + " is " + player.PlayerMelee_GetState() + "\n" ) +#endif + return + } + + if ( !IsAlive( player ) ) + { +#if SERVER + print( "SERVER: " + player + " is dead\n" ) +#else + print( "CLIENT: " + player + " is dead\n" ) +#endif + return + } + + thread CodeCallback_OnMeleePressed_InternalThread( player ) +} + +void function CodeCallback_OnMeleePressed_InternalThread( entity player ) +{ + if ( player.IsTitan() ) + { + TitanUnsyncedMelee( player ) + } + else if ( player.IsHuman() ) + { + const float STUN_EFFECT_CUTOFF = 0.05 + float movestunEffect = StatusEffect_Get( player, eStatusEffect.move_slow ) + bool movestunBlocked = (movestunEffect > STUN_EFFECT_CUTOFF) + + HumanUnsyncedMelee( player, movestunBlocked ) + } +} + +//void function CodeCallback_OnMeleeHeld( entity player ) +//{ +//} + +//void function CodeCallback_OnMeleeReleased( entity player ) +//{ +//} + +bool function ShouldHolsterWeaponForSyncedMelee( entity player ) +{ + if ( player.GetPlayerSettings() == "titan_ogre_minigun" ) + return false + + return ShouldHolsterWeaponForMelee( player ) +} + +bool function ShouldHolsterWeaponForMelee( entity player ) +{ + #if !SERVER + return true + #endif + + if ( !player.IsTitan() ) + return true + + return Time() - player.s.startDashMeleeTime > 1.0 //Fix issue with gun being out when it shouldn't, according to Mackey... +} + +#if SERVER +bool function NPCTriesSyncedMeleeVsPlayer( entity npc, entity player ) +{ + Assert( npc.IsNPC() ) + Assert( player.IsPlayer() ) + Assert( IsAlive( player ) ) + Assert( player.IsPlayer() ) + Assert( IsPilot( player ) ) + if ( player.ContextAction_IsBusy() ) + return false + + //#if SERVER + //player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION ) + //#else + //player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION_PREDICTED ) + //#endif + + return DoSyncedMelee( npc, player ) +} +#endif + + + +bool function PlayerTriesSyncedMelee( entity player, entity target ) +{ + if ( !target ) + return false + if ( !IsAlive( target ) ) + return false + + if ( target.ContextAction_IsBusy() ) + return false + + if ( player.IsTitan() ) + { +#if SERVER + player.PlayerMelee_SetState( PLAYER_MELEE_STATE_TITAN_EXECUTION ) +#else + player.PlayerMelee_SetState( PLAYER_MELEE_STATE_TITAN_EXECUTION_PREDICTED ) +#endif + } + else + { +#if SERVER + player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION ) +#else + player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION_PREDICTED ) +#endif + } + + if ( !player.Lunge_IsActive() || !player.Lunge_IsGroundExecute() || !player.Lunge_IsLungingToEntity() || (player.Lunge_GetTargetEntity() != target) ) + { +#if SERVER + print( "SERVER: " + player + " is calling Lunge_SetTargetEntity() from PlayerTriesSyncedMelee()\n" ) +#else + print( "CLIENT: " + player + " is calling Lunge_SetTargetEntity() from PlayerTriesSyncedMelee()\n" ) +#endif + player.Lunge_SetTargetEntity( target, false ) + } + +#if SERVER + OnThreadEnd( + function() : ( player, target ) + { + if ( IsValid( player ) && player.IsPlayer() ) + { + RemoveCinematicFlag( player, CE_FLAG_TITAN_3P_CAM ) + RemoveCinematicFlag( player, CE_FLAG_EXECUTION ) + } + if ( IsValid( target ) && target.IsPlayer() ) + { + RemoveCinematicFlag( target, CE_FLAG_TITAN_3P_CAM ) + RemoveCinematicFlag( target, CE_FLAG_EXECUTION ) + } + } + ) + + if ( player.IsTitan() ) + TransferDamageHistoryToTarget( target ) + if ( player.IsPlayer() ) + { + AddCinematicFlag( player, CE_FLAG_TITAN_3P_CAM ) + AddCinematicFlag( player, CE_FLAG_EXECUTION ) + } + if ( IsValid( target ) && target.IsPlayer() ) + { + AddCinematicFlag( target, CE_FLAG_TITAN_3P_CAM ) + AddCinematicFlag( player, CE_FLAG_EXECUTION ) + } +#endif + + bool success = DoSyncedMelee( player, target ) + if ( !success ) + { + player.Lunge_ClearTarget() + } + + return success +} + +function TransferDamageHistoryToTarget( entity target ) +{ + entity titanSoul = target.GetTitanSoul() + target.e.recentDamageHistory = titanSoul.e.recentDamageHistory +} + +bool function DoSyncedMelee( entity player, entity target ) +{ + SyncedMeleeChooser ornull actions = GetSyncedMeleeChooserForPlayerVsTarget( player, target ) + + Assert( actions != null, "No melee action for " + player + " vs " + target ) + expect SyncedMeleeChooser( actions ) + +#if SERVER + if ( player.IsPlayer() ) + { + PlayerMelee_StartLagCompensateTargetForLunge( player, target ) + } +#endif // #if SERVER + + SyncedMelee ornull action = FindBestSyncedMelee( player, target, actions ) + +#if SERVER + if ( player.IsPlayer() ) + { + + player.ForceStand() + player.UnforceStand() + PlayerMelee_FinishLagCompensateTarget( player ) + } +#endif // #if SERVER + + if ( action == null ) + return false + + expect SyncedMelee( action ) + + + + player.Signal( "OnSyncedMelee" ) + target.Signal( "OnSyncedMelee" ) + player.Signal( "OnSyncedMeleeAttacker" ) + target.Signal( "OnSyncedMeleeVictim" ) + +#if SERVER + player.p.lastExecutionUsed = action.ref + + if ( actions in file.syncedMeleeServerCallbacks ) + thread SyncedMeleeServerCallbacks( actions, action, player, target ) + bool functionref( SyncedMelee action, entity player, entity target ) think = file.syncedMeleeServerThink[ actions ] + return think( action, player, target ) +#endif // #if SERVER + + return true +} + +void function SyncedMeleeServerCallbacks( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target ) +{ + // Added via AddSyncedMeleeServerCallback + foreach ( index, _ in file.syncedMeleeServerCallbacks[ actions ] ) + { + void functionref( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target ) item = file.syncedMeleeServerCallbacks[ actions ][ index ] + item( actions, action, player, target ) + } +} + +/* +void function CodeCallback_OnMeleeReleased( entity player ) +{ +} +*/ + +function TextDebug( string msg ) +{ + wait 0.5 + printt( msg ) +} + +bool function ShouldClampTargetVelocity( vector targetVelocity, vector pushBackVelocity, float clampRatio ) +{ + float dot = DotProduct( targetVelocity, pushBackVelocity ) + if ( dot < 0 ) + return true + + if ( dot <= 0 ) + return false + + float velRatio = LengthSqr( targetVelocity ) / LengthSqr( pushBackVelocity ) + + return velRatio < clampRatio +} + +bool function CanBeMeleed( entity target ) +{ + if ( target.IsPlayer() ) + return true + if ( target.IsNPC() ) + return true + + if ( ObjectCanBeMeleed( target ) ) + return true + + return false +} + +// IMPORTANT: Only used for non-player, non-living special cases like prop_dynamics we want to be able to melee (drones, etc) +bool function ObjectCanBeMeleed( entity ent ) +{ + if ( !( "canBeMeleed" in ent.s ) ) + return false + + return expect bool( ent.s.canBeMeleed ) +} + +// IMPORTANT: Only used for non-player, non-living special cases like prop_dynamics we want to be able to melee (drones, etc) +function SetObjectCanBeMeleed( entity ent, bool value ) +{ + Assert( !ent.IsPlayer(), ent + " should not be a player. This is for non-player, non-NPC entities.") + Assert( !ent.IsNPC(), ent + " should not be an NPC. This is for non-player, non-NPC entities.") + + if ( !( "canBeMeleed" in ent.s ) ) + ent.s.canBeMeleed <- false + + ent.s.canBeMeleed = value +} + +//function TitanExposionDeath( entity titan, entity attacker ) +//{ +// if ( !IsAlive( titan ) ) +// return +// +// ExplodeTitanBits( titan ) +// // and your pretty titan too! +// +// //TitanEjectExplosion +// table deathTable = { scriptType = damageTypes.titanMelee, forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = eDamageSourceId.titan_execution } +// titan.TakeDamage( titan.GetMaxHealth() + 1, attacker, attacker, deathTable ) +//} + +#if SERVER +vector function GetRefAnglesBetweenEnts( entity attacker, entity target ) +{ + vector endOrigin = target.GetOrigin() + vector startOrigin = attacker.GetOrigin() + vector refVec = endOrigin - startOrigin + vector refAng = VectorToAngles( refVec ) + if ( fabs( AngleNormalize( refAng.x ) ) > 35 ) //If pitch is too much, use angles from either attacker or target + { + if ( attacker.IsTitan() ) + refAng = attacker.GetAngles() //Doing titan synced kill from front, so use attacker's origin + else + refAng = target.GetAngles() // Doing rear necksnap, so use target's angles + } + return refAng +} + +entity function CreateMeleeScriptMoverBetweenEnts( entity attacker, entity target ) +{ + vector refAng = GetRefAnglesBetweenEnts( attacker, target ) + + vector endOrigin = target.GetOrigin() + vector startOrigin = attacker.GetOrigin() + vector refVec = endOrigin - startOrigin + vector refPos = endOrigin - refVec * 0.5 + + entity ref = CreateOwnedScriptMover( attacker ) + ref.SetOrigin( refPos ) + ref.SetAngles( refAng ) + + return ref +} +#endif // SERVER + +void function AddSyncedMelee( SyncedMeleeChooser chooser, SyncedMelee melee ) +{ + // sqr the distance + melee.distanceSqr = melee.distance * melee.distance + + chooser.syncedMelees.append( melee ) +} + +SyncedMelee ornull function FindBestSyncedMelee( entity attacker, entity target, SyncedMeleeChooser actions ) +{ + #if CLIENT + Assert( attacker == GetLocalViewPlayer() ) + #endif // CLIENT + + vector absTargetToPlayerDir + if ( attacker.IsPlayer() && attacker.Lunge_IsActive() && (attacker.Lunge_GetTargetEntity() == target) ) + { + absTargetToPlayerDir = attacker.Lunge_GetStartPositionOffset() + absTargetToPlayerDir = Normalize( absTargetToPlayerDir ) + } + else + { + vector attackerPos = actions.attackerOriginFunc( attacker ) // + ( attacker.GetVelocity() * SMOOTH_TIME ) + vector targetPos = actions.targetOriginFunc( target ) + + if ( attackerPos == targetPos ) + { + absTargetToPlayerDir = < 1, 0, 0 > + } + else + { + absTargetToPlayerDir = Normalize( attackerPos - targetPos ) + } + } + + vector angles = attacker.EyeAngles() + vector forward = AnglesToForward( angles ) + + vector relTargetToPlayerDir = CalcRelativeVector( < 0, target.EyeAngles().y, 0 >, absTargetToPlayerDir ) + + array bestActions + float bestDot = -2.0 + float distSqr = LengthSqr( actions.attackerOriginFunc( attacker ) - actions.targetOriginFunc( target ) ) + + bool npcTarget = target.IsNPC() + bool playerAttacker = attacker.IsPlayer() + + int health = target.GetHealth() + float healthRatio = HealthRatio( target ) + int meleeDamage + if ( attacker.IsNPC() ) + { + meleeDamage = attacker.GetMeleeDamageMaxForTarget( target ) + } + else if ( attacker.IsPlayer() ) + { + meleeDamage = GetPlayerMeleeDamage( attacker ) + } + + SyncedMelee ornull returnVal = null + +#if MP + if ( IsPilot( attacker ) ) + { + PilotLoadoutDef loadout = GetActivePilotLoadout( attacker ) + + foreach ( action in actions.syncedMelees ) + { + if ( action.ref != loadout.execution ) + continue + + if ( npcTarget && !action.canTargetNPCs ) + break + + if ( playerAttacker && !action.usableByPlayers ) + break + + if ( healthRatio < action.targetMinHealthRatio ) + break + + if ( healthRatio > action.targetMaxHealthRatio ) + break + + if ( action.onlyIfLethal && health > meleeDamage ) + break + + if ( distSqr > action.distanceSqr ) + break + + float dot = relTargetToPlayerDir.Dot( action.direction ) + if ( dot < action.minDot ) + break + +#if SERVER + //Random Execution + if ( string( attacker.GetPersistentVar( "activePilotLoadout.execution" )) == "execution_random") + { + returnVal = PickRandomExecution(actions, attacker) + break + } +#endif + + returnVal = action + break + } + } + else + { +#endif + foreach ( action in actions.syncedMelees ) + { + if ( !action.enabled ) + continue + + if ( npcTarget && !action.canTargetNPCs ) + continue + + if ( playerAttacker && !action.usableByPlayers ) + continue + + if ( healthRatio < action.targetMinHealthRatio ) + continue + + if ( healthRatio > action.targetMaxHealthRatio ) + continue + + if ( action.onlyIfLethal && health > meleeDamage ) + continue + + if ( distSqr > action.distanceSqr ) + continue + + float dot = relTargetToPlayerDir.Dot( action.direction ) + + //printt( "Dot: " + dot ) + + if ( dot < action.minDot ) + continue + + if ( dot == bestDot ) + { + bestActions.append( action ) + continue + } + + if ( dot > bestDot ) + { + // found new best dot + bestActions.clear() + bestDot = dot + bestActions.append( action ) + } + } + + if ( bestActions.len() ) + returnVal = bestActions.getrandom() +#if MP + } +#endif + + return returnVal +} + +string function GetAttackerSyncedMelee( entity ent ) +{ + if ( ent.IsPlayer() ) + { + // TODO: for MP, change this to be based on loadout choice + string bodyType = GetPlayerBodyType( ent ) + if ( bodyType == "human" ) + { + entity weapon = ent.GetActiveWeapon() + var weaponSyncedMelee + + if ( IsValid( weapon ) ) + weaponSyncedMelee = weapon.GetWeaponInfoFileKeyField( "synced_melee_action" ) + + if ( weaponSyncedMelee ) + return string( weaponSyncedMelee ) + } + + return bodyType + + } + else if ( IsProwler( ent ) ) + { + return "prowler" + } + else if ( IsPilotElite( ent ) ) + { + return "pilotelite" + } + else if ( IsSpectre( ent ) ) + { + return "spectre" + } + else if ( ent.IsNPC() ) + { + return ent.GetBodyType() + } + else if ( ent.IsTitan() ) + { + return "titan" + } + + unreachable +} + +string function GetVictimSyncedMeleeTargetType( entity ent ) +{ + string targetType + + if ( ent.IsPlayer() && GetPlayerBodyType( ent ) == "human" ) + { + targetType = "human" + } + else if ( IsProwler( ent ) ) + { + targetType = "prowler" + } + else if ( IsPilotElite( ent ) ) + { + targetType = "pilotelite" + } + else if ( ent.IsNPC() ) + { + targetType = ent.GetBodyType() + } + else if ( ent.IsTitan() ) + { + targetType = "titan" + } + else + { + Assert( 0, "Unknown ent type" ) + } + + return targetType +} + +SyncedMeleeChooser ornull function GetSyncedMeleeChooserForPlayerVsTarget( entity attacker, entity target ) +{ + string attackerType = GetAttackerSyncedMelee( attacker ) + string targetType = GetVictimSyncedMeleeTargetType( target ) + + if ( !( attackerType in file.syncedMeleeChoosers ) ) + return null + + if ( !( targetType in file.syncedMeleeChoosers[ attackerType ] ) ) + return null + + return file.syncedMeleeChoosers[ attackerType ][ targetType ] +} + +void function CodeCallback_OnMeleeAttackAnimEvent( entity player ) +{ + Assert( IsValid( player ) ) +#if SERVER + print( "SERVER: " + player + " is calling CodeCallback_OnMeleeAttackAnimEvent()\n" ) +#else + print( "CLIENT: " + player + " is calling CodeCallback_OnMeleeAttackAnimEvent()\n" ) +#endif + if ( player.PlayerMelee_IsAttackActive() ) + { + if ( player.IsTitan() ) + TitanMeleeAttack( player ) + else if ( player.IsHuman() ) + HumanMeleeAttack( player ) + } +} + +bool function IsInExecutionMeleeState( entity player ) +{ + local meleeState = player.PlayerMelee_GetState() + switch ( meleeState ) + { + case PLAYER_MELEE_STATE_HUMAN_EXECUTION_PREDICTED: + case PLAYER_MELEE_STATE_HUMAN_EXECUTION: + case PLAYER_MELEE_STATE_TITAN_EXECUTION_PREDICTED: + case PLAYER_MELEE_STATE_TITAN_EXECUTION: + return true + + default: + return false + } + + unreachable +} + +#if SERVER +void function InitMeleeAnimEventCallbacks( entity player ) +{ + AddAnimEvent( player, "screen_blackout", MeleeBlackoutScreen_AE ) +} + +void function MeleeBlackoutScreen_AE( entity player ) +{ + ScreenFadeToBlack( player, 0.7, 1.2 ) +} +#endif + +bool function ShouldPlayerExecuteTarget( entity player, entity target ) +{ + if ( player.IsTitan() ) + { + if ( !target.IsTitan() ) + return false + + if ( Flag( "ForceSyncedMelee" ) ) + return true + + if ( !GetDoomedState( target ) ) + return false + + entity soul = target.GetTitanSoul() + if ( soul != null ) + { + if ( soul.GetShieldHealth() > 0 && GetCurrentPlaylistVarInt( "titan_shield_blocks_execution", 0 ) != 0 ) + return false + } + + if ( !SyncedMelee_IsAllowed( player ) ) + return false + + return true + } + + if ( player.IsHuman() ) + { + if ( !IsHumanSized( target ) ) + return false + +#if SERVER + if ( Flag( "ForceSyncedMelee" ) ) + return true +#endif // #if SERVER + + if ( !SyncedMelee_IsAllowed( player ) ) + return false + } + + return true +} + +vector function ClampVerticalVelocity( vector targetVelocity, float maxVerticalVelocity ) +{ + vector clampedVelocity = targetVelocity + if ( clampedVelocity.z > maxVerticalVelocity ) + { + printt( "clampedVelocity.z: " + clampedVelocity.z +", maxVerticalVelocity:" + maxVerticalVelocity ) + clampedVelocity = Vector( targetVelocity.x, targetVelocity.y, maxVerticalVelocity ) + } + + return clampedVelocity +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool function CheckVerticallyCloseEnough( entity attacker, entity target ) +{ + vector attackerOrigin = attacker.GetOrigin() + vector targetOrigin = target.GetOrigin() + + float verticalDistance = fabs( attackerOrigin.z - targetOrigin.z ) + float halfHeight = 0 + + if ( attacker.IsTitan() ) + halfHeight = 92.5 + else if ( attacker.IsHuman() ) + halfHeight = 30 + + Assert( halfHeight, "Attacker is neither Titan nor Human" ) + + //printt( "vertical distance: " + verticalDistance ) + return verticalDistance < halfHeight +} + + +entity function GetLungeTargetForPlayer( entity player ) +{ + // Titan melee does not lunge + if ( player.IsTitan() ) + return null + + if ( player.IsPhaseShifted() ) + return null + + entity lungeTarget = PlayerMelee_LungeConeTrace( player, SHARED_CB_IS_VALID_MELEE_ATTACK_TARGET ) + return lungeTarget +} + +#if SERVER +void function Melee_Enable( entity player ) +{ + player.SetPlayerNetBool( "playerAllowedToMelee", true ) +} + +void function Melee_Disable( entity player ) +{ + player.SetPlayerNetBool( "playerAllowedToMelee", false ) +} + +void function SyncedMelee_Enable( entity player ) +{ + player.SetPlayerNetBool( "playerAllowedToSyncedMelee", true ) +} + +void function SyncedMelee_Disable( entity player ) +{ + player.SetPlayerNetBool( "playerAllowedToSyncedMelee", false ) +} +#endif + +bool function Melee_IsAllowed( entity player ) +{ + return player.GetPlayerNetBool( "playerAllowedToMelee" ) +} + +bool function SyncedMelee_IsAllowed( entity player ) +{ + return player.GetPlayerNetBool( "playerAllowedToSyncedMelee" ) +} + +bool function IsAttackerRef( SyncedMelee ornull action, entity target ) +{ + if ( action != null ) + { + expect SyncedMelee( action ) + if ( action.isAttackerRef ) + { + return true + } + } + + if ( !target ) + return true + + if ( !IsValid( target ) ) + return true + + if ( !target.IsPlayer() ) + return true + + return false +} + +#if MP +#if SERVER +SyncedMelee ornull function PickRandomExecution( SyncedMeleeChooser actions, entity attacker ) +{ + array possibleExecutions = [] + + SyncedMelee neckSnap + + foreach ( action in actions.syncedMelees ) + { + if (action.ref == "execution_neck_snap") + neckSnap = action + + if(!IsItemLocked( attacker, action.ref ) && action.ref != "execution_random" && action.ref != attacker.p.lastExecutionUsed) + + possibleExecutions.append(action) + } + + if (possibleExecutions.len() == 0) + return neckSnap + + possibleExecutions.randomize() + + return possibleExecutions[0] +} +#endif +#endif \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt index a1337d9f1..09aac6eae 100644 --- a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt +++ b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt @@ -102,6 +102,8 @@ WeaponData // Damage - When Used by Players "damage_type" "burn" + "show_grenade_indicator" "1" + "crosshair" "crosshair_t" "explosion_damage" "350" // 150 "explosion_damage_heavy_armor" "550" // 150 diff --git a/Northstar.CustomServers/mod.json b/Northstar.CustomServers/mod.json index e1df99ba8..3525c434c 100644 --- a/Northstar.CustomServers/mod.json +++ b/Northstar.CustomServers/mod.json @@ -1,7 +1,7 @@ { "Name": "Northstar.CustomServers", "Description": "Attempts to recreate the behaviour of vanilla Titanfall 2 servers, as well as changing some scripts to allow better support for mods", - "Version": "1.13.0", + "Version": "1.14.0", "LoadPriority": 0, "ConVars": [ { diff --git a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut index 9717c76d9..89fb7a826 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/ai/_ai_soldiers.gnut @@ -60,6 +60,19 @@ function AiSoldiers_Init() level.COOP_AT_WEAPON_RATES[ "mp_weapon_smr" ] <- 0.4 level.COOP_AT_WEAPON_RATES[ "mp_weapon_mgl" ] <- 0.1 + // add stub death callback, because in _codecallbacks_common.gnut there is + // CodeCallback_OnEntityKilled which is only called when an entity is being tracked. An + // entity is set to be tracked if it has a death callback for it's class, unfortunately this + // is then relayed to clients and used for client side death callbacks. The end result of + // not having this function called is that clients become completely unaware of any grunt + // deaths. A noticeable difference here is that grunts do not play the kill confirmed audio + // except on War Games, which does register a callback for grunt deaths to make them dissolve. + // + // Whilst this may seem like a bit of a hacky solution, it is generally better than simply + // tracking all entities. If a different callback is created in the future for grunt deaths + // that is not specific to a gamemode, map, etc. then this could be removed + AddDeathCallback( "npc_soldier", void function( entity guy, var damageInfo ){} ) + PrecacheSprite( $"sprites/glow_05.vmt" ) FlagInit( "disable_npcs" ) FlagInit( "Disable_IMC" ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut index 0fad768c5..4ed7ee4a0 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut @@ -1,7 +1,6 @@ global function AiGameModes_Init -global function AiGameModes_SetGruntWeapons -global function AiGameModes_SetSpectreWeapons +global function AiGameModes_SetNPCWeapons global function AiGameModes_SpawnDropShip global function AiGameModes_SpawnDropPod @@ -15,25 +14,20 @@ const INTRO_DROPSHIP_CUTOFF = 2000 struct { - array< string > gruntWeapons = [ "mp_weapon_rspn101" ] - array< string > spectreWeapons = [ "mp_weapon_hemlok_smg" ] + table< string, array > npcWeaponsTable // npcs have their default weapon in aisettings file } file void function AiGameModes_Init() { - } //------------------------------------------------------ -void function AiGameModes_SetGruntWeapons( array< string > weapons ) +void function AiGameModes_SetNPCWeapons( string npcClass, array weapons ) { - file.gruntWeapons = weapons -} - -void function AiGameModes_SetSpectreWeapons( array< string > weapons ) -{ - file.spectreWeapons = weapons + if ( !( npcClass in file.npcWeaponsTable ) ) + file.npcWeaponsTable[ npcClass ] <- [] + file.npcWeaponsTable[ npcClass ] = weapons } //------------------------------------------------------ @@ -59,7 +53,7 @@ void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int c foreach ( guy in guys ) { - ReplaceWeapon( guy, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] ) + SetUpNPCWeapons( guy ) guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) } @@ -68,31 +62,23 @@ void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int c } -void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array guys ) squadHandler = null ) +void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array guys ) squadHandler = null, int flags = 0 ) { - string squadName = MakeSquadName( team, UniqueString( "" ) ) - array guys - entity pod = CreateDropPod( pos, <0,0,0> ) - InitFireteamDropPod( pod ) - + InitFireteamDropPod( pod, flags ) + + waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot ) + + string squadName = MakeSquadName( team, UniqueString( "" ) ) + array guys for ( int i = 0; i < 4 ;i++ ) { entity npc = CreateNPC( content, team, pos,<0,0,0> ) DispatchSpawn( npc ) SetSquad( npc, squadName ) - switch ( content ) - { - case "npc_soldier": - ReplaceWeapon( npc, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] ) - break - - case "npc_spectre": - ReplaceWeapon( npc, file.spectreWeapons[ RandomInt( file.spectreWeapons.len() ) ], [] ) - break - } + SetUpNPCWeapons( npc ) npc.SetParent( pod, "ATTACH", true ) @@ -100,25 +86,26 @@ void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string guys.append( npc ) } - // The order here is different so we can show on minimap while were still falling + ActivateFireteamDropPod( pod, guys ) + + // start searching for enemies if ( squadHandler != null ) thread squadHandler( guys ) - - waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot ) - - ActivateFireteamDropPod( pod, guys ) } +const float REAPER_WARPFALL_DELAY = 4.7 // same as fd does void function AiGameModes_SpawnReaper( vector pos, vector rot, int team, string aiSettings = "", void functionref( entity reaper ) reaperHandler = null ) { - thread Reaper_Spawnpoint( pos, team, 11.2 ) + float reaperLandTime = REAPER_WARPFALL_DELAY + 1.2 // reaper takes ~1.2s to warpfall + thread HotDrop_Spawnpoint( pos, team, reaperLandTime, false, damagedef_reaper_fall ) - wait 10 - // spawn reapers right before it warpfalls, or round_end clean up will crash the game + wait REAPER_WARPFALL_DELAY entity reaper = CreateSuperSpectre( team, pos, rot ) + reaper.EndSignal( "OnDestroy" ) // reaper highlight Highlight_SetFriendlyHighlight( reaper, "sp_enemy_pilot" ) - reaper.Highlight_SetParam( 1, 0, < 3,3,3 > ) + reaper.Highlight_SetParam( 1, 0, < 1,1,1 > ) + SetDefaultMPEnemyHighlight( reaper ) Highlight_SetEnemyHighlight( reaper, "enemy_titan" ) SetSpawnOption_Titanfall( reaper ) @@ -127,15 +114,18 @@ void function AiGameModes_SpawnReaper( vector pos, vector rot, int team, string if ( aiSettings != "" ) SetSpawnOption_AISettings( reaper, aiSettings ) + HideName( reaper ) // prevent flash a name onto it DispatchSpawn( reaper ) - + + reaper.WaitSignal( "WarpfallComplete" ) + ShowName( reaper ) // show name again after drop if ( reaperHandler != null ) thread reaperHandler( reaper ) } // copied from cl_replacement_titan_hud.gnut -void function Reaper_Spawnpoint( vector origin, int team, float impactTime, bool hasFriendlyWarning = false ) +void function HotDrop_Spawnpoint( vector origin, int team, float impactTime, bool hasFriendlyWarning = false, int damageDef = -1 ) { array targetEffects = [] vector surfaceNormal = < 0, 0, 1 > @@ -146,32 +136,50 @@ void function Reaper_Spawnpoint( vector origin, int team, float impactTime, bool { entity effectFriendly = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal ) SetTeam( effectFriendly, team ) - EffectSetControlPointVector( effectFriendly, 1, < 128,188,255 > ) + EffectSetControlPointVector( effectFriendly, 1, FRIENDLY_COLOR_FX ) effectFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY + effectFriendly.DisableHibernation() // prevent it from fading out targetEffects.append( effectFriendly ) } entity effectEnemy = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal ) SetTeam( effectEnemy, team ) - EffectSetControlPointVector( effectEnemy, 1, < 255,99,0 > ) + EffectSetControlPointVector( effectEnemy, 1, ENEMY_COLOR_FX ) effectEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY + effectEnemy.DisableHibernation() // prevent it from fading out targetEffects.append( effectEnemy ) - + + // so enemy npcs will mostly avoid them + entity damageAreaInfo + if ( damageDef > -1 ) + { + damageAreaInfo = CreateEntity( "info_target" ) + DispatchSpawn( damageAreaInfo ) + AI_CreateDangerousArea_DamageDef( damageDef, damageAreaInfo, team, true, true ) + } + wait impactTime + // clean up foreach( entity targetEffect in targetEffects ) { if ( IsValid( targetEffect ) ) EffectStop( targetEffect ) } + if ( IsValid( damageAreaInfo ) ) + damageAreaInfo.Destroy() } // including aisettings stuff specifically for at bounty titans +const float TITANFALL_WARNING_DURATION = 5.0 void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string setFile, string aiSettings = "", void functionref( entity titan ) titanHandler = null ) { entity titan = CreateNPCTitan( setFile, TEAM_BOTH, pos, rot ) SetSpawnOption_Titanfall( titan ) SetSpawnOption_Warpfall( titan ) + + // modified: do a hotdrop spawnpoint warning + thread HotDrop_Spawnpoint( pos, team, TITANFALL_WARNING_DURATION, false, damagedef_titan_fall ) if ( aiSettings != "" ) SetSpawnOption_AISettings( titan, aiSettings ) @@ -182,12 +190,27 @@ void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string s thread titanHandler( titan ) } -// entity.ReplaceActiveWeapon gave grunts archers sometimes, this is my replacement for it -void function ReplaceWeapon( entity guy, string weapon, array mods ) +void function SetUpNPCWeapons( entity guy ) { - guy.TakeActiveWeapon() - guy.GiveWeapon( weapon, mods ) - guy.SetActiveWeaponByName( weapon ) + string className = guy.GetClassName() + + array mainWeapons + if ( className in file.npcWeaponsTable ) + mainWeapons = file.npcWeaponsTable[ className ] + + if ( mainWeapons.len() == 0 ) // no valid weapons + return + + // take off existing main weapons, or sometimes they'll have a archer by default + foreach ( entity weapon in guy.GetMainWeapons() ) + guy.TakeWeapon( weapon.GetWeaponClassName() ) + + if ( mainWeapons.len() > 0 ) + { + string weaponName = mainWeapons[ RandomInt( mainWeapons.len() ) ] + guy.GiveWeapon( weaponName ) + guy.SetActiveWeaponByName( weaponName ) + } } // Checks if we can spawn a dropship at a node, this should guarantee dropship ziplines diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut index fae778d6d..f47ee90f9 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut @@ -1,22 +1,36 @@ untyped global function GamemodeAITdm_Init -const SQUADS_PER_TEAM = 3 +// these are now default settings +const int SQUADS_PER_TEAM = 4 -const REAPERS_PER_TEAM = 2 +const int REAPERS_PER_TEAM = 2 -const LEVEL_SPECTRES = 125 -const LEVEL_STALKERS = 380 -const LEVEL_REAPERS = 500 +const int LEVEL_SPECTRES = 125 +const int LEVEL_STALKERS = 380 +const int LEVEL_REAPERS = 500 + +// add settings +global function AITdm_SetSquadsPerTeam +global function AITdm_SetReapersPerTeam +global function AITdm_SetLevelSpectres +global function AITdm_SetLevelStalkers +global function AITdm_SetLevelReapers struct { // Due to team based escalation everything is an array - array< int > levels = [ LEVEL_SPECTRES, LEVEL_SPECTRES ] + array< int > levels = [] // Initilazed in `Spawner_Threaded` array< array< string > > podEntities = [ [ "npc_soldier" ], [ "npc_soldier" ] ] array< bool > reapers = [ false, false ] -} file + // default settings + int squadsPerTeam = SQUADS_PER_TEAM + int reapersPerTeam = REAPERS_PER_TEAM + int levelSpectres = LEVEL_SPECTRES + int levelStalkers = LEVEL_STALKERS + int levelReapers = LEVEL_REAPERS +} file void function GamemodeAITdm_Init() { @@ -34,18 +48,47 @@ void function GamemodeAITdm_Init() if ( GetCurrentPlaylistVarInt( "aitdm_archer_grunts", 0 ) == 0 ) { - AiGameModes_SetGruntWeapons( [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_hemlok_smg", "mp_weapon_lstar", "mp_weapon_mastiff" ] ) } else { - AiGameModes_SetGruntWeapons( [ "mp_weapon_rocket_launcher" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_rocket_launcher" ] ) } ScoreEvent_SetupEarnMeterValuesForMixedModes() } +// add settings +void function AITdm_SetSquadsPerTeam( int squads ) +{ + file.squadsPerTeam = squads +} + +void function AITdm_SetReapersPerTeam( int reapers ) +{ + file.reapersPerTeam = reapers +} + +void function AITdm_SetLevelSpectres( int level ) +{ + file.levelSpectres = level +} + +void function AITdm_SetLevelStalkers( int level ) +{ + file.levelStalkers = level +} + +void function AITdm_SetLevelReapers( int level ) +{ + file.levelReapers = level +} +// + // Starts skyshow, this also requiers AINs but doesn't crash if they're missing void function OnPrematchStart() { @@ -74,11 +117,9 @@ void function HandleScoreEvent( entity victim, entity attacker, var damageInfo ) // Basic checks if ( victim == attacker || !( attacker.IsPlayer() || attacker.IsTitan() ) || GetGameState() != eGameState.Playing ) return - // Hacked spectre filter if ( victim.GetOwner() == attacker ) return - // NPC titans without an owner player will not count towards any team's score if ( attacker.IsNPC() && attacker.IsTitan() && !IsValid( GetPetTitanOwner( attacker ) ) ) return @@ -193,7 +234,7 @@ void function SpawnIntroBatch_Threaded( int team ) int ships = shipNodes.len() - for ( int i = 0; i < SQUADS_PER_TEAM; i++ ) + for ( int i = 0; i < file.squadsPerTeam; i++ ) { if ( pods != 0 || ships == 0 ) { @@ -238,6 +279,7 @@ void function Spawner_Threaded( int team ) // used to index into escalation arrays int index = team == TEAM_MILITIA ? 0 : 1 + file.levels = [ file.levelSpectres, file.levelSpectres ] // due we added settings, should init levels here! while( true ) { @@ -252,7 +294,7 @@ void function Spawner_Threaded( int team ) if ( file.reapers[ index ] ) { array< entity > points = SpawnPoints_GetDropPod() - if ( reaperCount < REAPERS_PER_TEAM ) + if ( reaperCount < file.reapersPerTeam ) { entity node = points[ GetSpawnPointIndex( points, team ) ] waitthread AiGameModes_SpawnReaper( node.GetOrigin(), node.GetAngles(), team, "npc_super_spectre_aitdm", ReaperHandler ) @@ -260,7 +302,7 @@ void function Spawner_Threaded( int team ) } // NORMAL SPAWNS - if ( count < SQUADS_PER_TEAM * 4 - 2 ) + if ( count < file.squadsPerTeam * 4 - 2 ) { string ent = file.podEntities[ index ][ RandomInt( file.podEntities[ index ].len() ) ] @@ -306,19 +348,19 @@ void function Escalate( int team ) // Based on score escalate a team switch ( file.levels[ index ] ) { - case LEVEL_SPECTRES: - file.levels[ index ] = LEVEL_STALKERS + case file.levelSpectres: + file.levels[ index ] = file.levelStalkers file.podEntities[ index ].append( "npc_spectre" ) SetGlobalNetInt( defcon, 2 ) return - case LEVEL_STALKERS: - file.levels[ index ] = LEVEL_REAPERS + case file.levelStalkers: + file.levels[ index ] = file.levelReapers file.podEntities[ index ].append( "npc_stalker" ) SetGlobalNetInt( defcon, 3 ) return - case LEVEL_REAPERS: + case file.levelReapers: file.reapers[ index ] = true SetGlobalNetInt( defcon, 4 ) return @@ -355,30 +397,47 @@ int function GetSpawnPointIndex( array< entity > points, int team ) // AI can also flee deeper into their zone suggesting someone spent way too much time on this void function SquadHandler( array guys ) { + int team = guys[0].GetTeam() + // show the squad enemy radar + array players = GetPlayerArrayOfEnemies( team ) + foreach ( entity guy in guys ) + { + if ( IsAlive( guy ) ) + { + foreach ( player in players ) + guy.Minimap_AlwaysShow( 0, player ) + } + } + // Not all maps have assaultpoints / have weird assault points ( looking at you ac ) // So we use enemies with a large radius - array< entity > points = GetNPCArrayOfEnemies( guys[0].GetTeam() ) - - if ( points.len() == 0 ) + while ( GetNPCArrayOfEnemies( team ).len() == 0 ) // if we can't find any enemy npcs, keep waiting + WaitFrame() + + // our waiting is end, check if any soldiers left + bool squadAlive = false + foreach ( entity guy in guys ) + { + if ( IsAlive( guy ) ) + squadAlive = true + else + guys.removebyvalue( guy ) + } + if ( !squadAlive ) return + + array points = GetNPCArrayOfEnemies( team ) vector point point = points[ RandomInt( points.len() ) ].GetOrigin() - array players = GetPlayerArrayOfEnemies( guys[0].GetTeam() ) - - // Setup AI + // Setup AI, first assault point foreach ( guy in guys ) { guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) guy.AssaultPoint( point ) guy.AssaultSetGoalRadius( 1600 ) // 1600 is minimum for npc_stalker, works fine for others - - // show on enemy radar - foreach ( player in players ) - guy.Minimap_AlwaysShow( 0, player ) - - + //thread AITdm_CleanupBoredNPCThread( guy ) } @@ -396,16 +455,32 @@ void function SquadHandler( array guys ) // Stop func if our squad has been killed off if ( guys.len() == 0 ) return + } + + // Get point and send our whole squad to it + points = GetNPCArrayOfEnemies( team ) + if ( points.len() == 0 ) // can't find any points here + { + // Have to wait some amount of time before continuing + // because if we don't the server will continue checking this + // forever, aren't loops fun? + // This definitely didn't waste ~8 hours of my time reverting various + // launcher PRs before finding this mods PR that caused servers to + // freeze forever before having their process killed by the dedi watchdog + // without any logging. If anyone reads this, PLEASE add logging to your scripts + // for when weird edge cases happen, it can literally only help debugging. -Spoon + WaitFrame() + continue + } - // Get point and send guy to it - points = GetNPCArrayOfEnemies( guy.GetTeam() ) - if ( points.len() == 0 ) - continue - - point = points[ RandomInt( points.len() ) ].GetOrigin() - - guy.AssaultPoint( point ) + point = points[ RandomInt( points.len() ) ].GetOrigin() + + foreach ( guy in guys ) + { + if ( IsAlive( guy ) ) + guy.AssaultPoint( point ) } + wait RandomFloatRange(5.0,15.0) } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut index 915e03e08..93a3aa168 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut @@ -1,12 +1,54 @@ +untyped // AddCallback_OnUseEntity() needs this global function GamemodeAt_Init global function RateSpawnpoints_AT -const int BH_AI_TEAM = TEAM_BOTH -const int BOUNTY_TITAN_DAMAGE_POOL = 400 // Rewarded for damage -const int BOUNTY_TITAN_KILL_REWARD = 100 // Rewarded for kill -const float WAVE_STATE_TRANSITION_TIME = 5.0 +// Old bobr note which still applies after a year :) +// IMPLEMENTATION NOTES: +// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives) +// in the interest of time, this script isn't gonna support any of that atm +// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead +// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience + + +// Bank settings +const float AT_BANKS_OPEN_DURATION = 45.0 // Bank open time +const int AT_BANK_DEPOSIT_RATE = 10 // Amount deposited per second +const int AT_BANK_DEPOSIT_RADIUS = 256 // bank radius for depositing +const float AT_BANK_FORCE_CLOSE_DELAY = 4.0 // If all bonus money has been deposited close the banks after this constant early + +// TODO: The reference function no longer exists, check if this still holds true +// VoyageDB: HACK score events... respawn made things in AT_SetScoreEventOverride() really messed up, have to do some hack here +const array AT_ENABLE_SCOREEVENTS = +[ + // these are disabled in AT_SetScoreEventOverride(), but related scoreEvents are not implemented into gamemode + // needs to re-enable them + "DoomTitan", + "DoomAutoTitan" +] +const array AT_DISABLE_SCOREEVENTS = +[ + // these are missed in AT_SetScoreEventOverride(), but game actually used them + // needs to disable them + "KillStalker" +] + +// Wave settings +// General +const int AT_AI_TEAM = TEAM_BOTH // Allow AI to attack and be attacked by both player teams +const float AT_FIRST_WAVE_START_DELAY = 10.0 // First wave has an extra delay before begining +const float AT_WAVE_TRANSITION_DELAY = 5.0 // Time between each wave and banks opening/closing +const float AT_WAVE_END_ANNOUNCEMENT_DELAY = 1.0 // Extra wait before announcing wave cleaned + +// Squad settings +const int AT_DROPPOD_SQUADS_ALLOWED_ON_FIELD = 4 // default is 4 droppod squads on field, won't use if AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK turns on // TODO: verify this + +// Titan bounty settings +const float AT_BOUNTY_TITAN_CHECK_DELAY = 10.0 // wait for bounty titans landing before we start checking their life state +const float AT_BOUNTY_TITAN_HEALTH_MULTIPLIER = 3 // TODO: Verify this -const array VALID_BOUNTY_TITAN_SETTINGS = [ +// Titan boss settings, check sh_gamemode_at.nut for more info +const array AT_BOUNTY_TITANS_AI_SETTINGS = +[ "npc_titan_atlas_stickybomb_bounty", "npc_titan_atlas_tracker_bounty", "npc_titan_ogre_minigun_bounty", @@ -16,302 +58,1564 @@ const array VALID_BOUNTY_TITAN_SETTINGS = [ "npc_titan_atlas_vanguard_bounty" ] +// Extra +// Respawn didn't use the "totalAllowedOnField" for npc spawning, they only allow 1 squad to be on field for each type of npc. enabling this might cause too much npcs spawning and crash the game +const bool AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK = false + +// Objectives +const int AT_OBJECTIVE_EMPTY = -1 // Remove objective +const int AT_OBJECTIVE_KILL_DZ = 104 // #AT_OBJECTIVE_KILL_DZ +const int AT_OBJECTIVE_KILL_DZ_MULTI = 105 // #AT_OBJECTIVE_KILL_DZ_MULTI +const int AT_OBJECTIVE_KILL_BOSS = 106 // #AT_OBJECTIVE_KILL_BOSS +const int AT_OBJECTIVE_KILL_BOSS_MULTI = 107 // #AT_OBJECTIVE_KILL_BOSS_MULTI +const int AT_OBJECTIVE_BANK_OPEN = 109 // #AT_BANK_OPEN_OBJECTIVE + +// When a player tries to deposit when they have 0 bonus money +// we show a help mesage, this is the ratelimit for that message +// so that we dont spam it too much +const float AT_PLAYER_HUD_MESSAGE_COOLDOWN = 2.5 + +// Due to bad navmeshes NPCs may wonder off to bumfuck nowhere or the game +// might teleport them into the map while trying to correct their position +// This obviously breaks bounty hunt where the objective is to kill ALL ai +// so we try to cleanup the camps after a set amount of time of inactivity +const int AT_CAMP_BORED_NPCS_LEFT_TO_START_CLEANUP = 3 +const float AT_CAMP_BORED_CLEANUP_WAIT = 60.0 +struct +{ + array banks + array camps + + // Used to track ScriptmanagedEntArrays of ai squads + table< int, array > campScriptEntArrays + + table< entity, bool > titanIsBountyBoss + table< entity, int > bountyTitanRewards + table< entity, int > npcStolenBonus + table< entity, bool > playerBankUploading + table< entity, table > playerSavedBountyDamage + table< entity, float > playerHudMessageAllowedTime +} file + +void function GamemodeAt_Init() +{ + // wave + RegisterSignal( "ATWaveEnd" ) + // camp + RegisterSignal( "ATCampClean" ) + RegisterSignal( "ATAllCampsClean" ) + + // Set-up score callbacks + ScoreEvent_SetupEarnMeterValuesForMixedModes() + AddCallback_OnPlayerKilled( AT_PlayerOrNPCKilledScoreEvent ) + AddCallback_OnNPCKilled( AT_PlayerOrNPCKilledScoreEvent ) + + // Set npc weapons + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_hemlok_smg", "mp_weapon_lstar", "mp_weapon_mastiff" ] ) + + // Gamestate callbacks + AddCallback_GameStateEnter( eGameState.Prematch, OnATGamePrematch ) + AddCallback_GameStateEnter( eGameState.Playing, OnATGamePlaying ) + + // Initilaze player + AddCallback_OnClientConnected( InitialiseATPlayer ) + + // Initilaze gamemode entities + AddCallback_EntitiesDidLoad( OnEntitiesDidLoad ) +} + +void function RateSpawnpoints_AT( int checkclass, array spawnpoints, int team, entity player ) +{ + RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) +} + + + +//////////////////////////////////////// +///// GAMESTATE CALLBACK FUNCTIONS ///// +//////////////////////////////////////// + +void function OnATGamePrematch() +{ + AT_ScoreEventsValueSetUp() +} + +void function OnATGamePlaying() +{ + thread AT_GameLoop_Threaded() +} + +//////////////////////////////////////////// +///// GAMESTATE CALLBACK FUNCTIONS END ///// +//////////////////////////////////////////// + + + +//////////////////////////// +///// PLAYER FUNCTIONS ///// +//////////////////////////// + +void function InitialiseATPlayer( entity player ) +{ + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_OnPlayerConnected" ) + player.SetPlayerNetInt( "AT_bonusPointMult", 1 ) + file.playerBankUploading[ player ] <- false + file.playerSavedBountyDamage[ player ] <- {} + file.playerHudMessageAllowedTime[ player ] <- 0.0 + thread AT_PlayerTitleThink( player ) + thread AT_PlayerObjectiveThink( player ) +} + +void function AT_PlayerTitleThink( entity player ) +{ + player.EndSignal( "OnDestroy" ) + + while ( true ) + { + if ( GetGameState() == eGameState.Playing ) + { + // Set player money count + player.SetTitle( "$" + string( AT_GetPlayerBonusPoints( player ) ) ) + } + else if ( GetGameState() >= eGameState.WinnerDetermined ) + { + if ( player.IsTitan() ) + player.SetTitle( GetTitanPlayerTitle( player ) ) + else + player.SetTitle( "" ) + + return + } + + WaitFrame() + } +} + +string function GetTitanPlayerTitle( entity player ) +{ + entity soul = player.GetTitanSoul() + + if ( !IsValid( soul ) ) + return "" + + string settings = GetSoulPlayerSettings( soul ) + var title = GetPlayerSettingsFieldForClassName( settings, "printname" ) + + if ( title == null ) + return "" + + return expect string( title ) +} + +void function AT_PlayerObjectiveThink( entity player ) +{ + player.EndSignal( "OnDestroy" ) + + int curObjective = AT_OBJECTIVE_EMPTY + while ( true ) + { + // game entered other state + if ( GetGameState() >= eGameState.WinnerDetermined ) + { + player.SetPlayerNetInt( "gameInfoStatusText", AT_OBJECTIVE_EMPTY ) + return + } + + int nextObjective = AT_OBJECTIVE_EMPTY + + // Determine objective text for player + if ( !IsAlive( player ) ) // Don't show objective to dead players + { + nextObjective = AT_OBJECTIVE_EMPTY + } + else // We're still alive + { + if ( GetGlobalNetBool( "banksOpen" ) ) + { + nextObjective = AT_OBJECTIVE_BANK_OPEN + } + else if ( GetGlobalNetBool( "preBankPhase" ) ) + { + nextObjective = AT_OBJECTIVE_EMPTY + } + else + { + // No checks have passed, try to do a "Kill all x near the marked dropzone" objective + int dropZoneActiveCount = 0 + int bossAliveCount = 0 + array campEnts + campEnts.append( GetGlobalNetEnt( "camp1Ent" ) ) + campEnts.append( GetGlobalNetEnt( "camp2Ent" ) ) + + foreach ( entity ent in campEnts ) + { + if ( IsValid( ent ) ) + { + if ( ent.IsTitan() ) + bossAliveCount += 1 + else + dropZoneActiveCount += 1 + } + } + + switch( dropZoneActiveCount ) + { + case 1: + nextObjective = AT_OBJECTIVE_KILL_DZ + break + case 2: + nextObjective = AT_OBJECTIVE_KILL_DZ_MULTI + break + } + + switch( bossAliveCount ) + { + case 1: + nextObjective = AT_OBJECTIVE_KILL_BOSS + break + case 2: + nextObjective = AT_OBJECTIVE_KILL_BOSS_MULTI + break + } + + // We couldn't get an objective, set it to empty + if ( dropZoneActiveCount == 0 && bossAliveCount == 0 ) + nextObjective = AT_OBJECTIVE_EMPTY + } + } + + // Set the objective when changed + if ( curObjective != nextObjective ) + { + player.SetPlayerNetInt( "gameInfoStatusText", nextObjective ) + curObjective = nextObjective + } + + WaitFrame() + } +} + +//////////////////////////////// +///// PLAYER FUNCTIONS END ///// +//////////////////////////////// + + + +//////////////////////////////////////// +///// GAMEMODE INITILAZE FUNCTIONS ///// +//////////////////////////////////////// + +void function OnEntitiesDidLoad() +{ + foreach ( entity info_target in GetEntArrayByClass_Expensive( "info_target" ) ) + { + if( info_target.HasKey( "editorclass" ) ) + { + switch( info_target.kv.editorclass ) + { + case "info_attrition_bank": + entity bank = CreateEntity( "prop_script" ) + bank.SetScriptName( "AT_Bank" ) // VoyageDB: don't know how to make client able to track it + bank.SetOrigin( info_target.GetOrigin() ) + bank.SetAngles( info_target.GetAngles() ) + DispatchSpawn( bank ) + bank.kv.solid = SOLID_VPHYSICS + bank.SetModel( info_target.GetModelName() ) + + // Minimap icon init + bank.Minimap_SetCustomState( eMinimapObject_prop_script.AT_BANK ) + bank.Minimap_SetAlignUpright( true ) + bank.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + bank.Minimap_Hide( TEAM_IMC, null ) + bank.Minimap_Hide( TEAM_MILITIA, null ) + + // Create tracker ent + // we don't need to store these at all, client just needs to get them + DispatchSpawn( GetAvailableBankTracker( bank ) ) + + // Make sure the bank is in it's disabled pose + thread PlayAnim( bank, "mh_inactive_idle" ) + // Set the bank usable + AddCallback_OnUseEntity( bank, OnPlayerUseBank ) + bank.SetUsable() + bank.SetUsePrompts( "#AT_USE_BANK_CLOSED", "#AT_USE_BANK_CLOSED" ) + + file.banks.append( bank ) + break; + case "info_attrition_camp": + AT_WaveOrigin campStruct + campStruct.ent = info_target + campStruct.origin = info_target.GetOrigin() + campStruct.radius = expect string( info_target.kv.radius ).tofloat() + campStruct.height = expect string( info_target.kv.height ).tofloat() + + // Assumes every info_attrition_camp will have all 9 phases, possibly not a good idea? + // TODO: verify this on all vanilla maps before release + for ( int i = 0; i < 9; i++ ) + campStruct.phaseAllowed.append( expect string( info_target.kv[ "phase_" + ( i + 1 ) ] ) == "1" ) + + // Get droppod spawns within the camp + foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) + { + vector campPos = info_target.GetOrigin() + vector spawnPos = spawnpoint.GetOrigin() + if ( Distance( campPos, spawnPos ) < campStruct.radius ) + campStruct.dropPodSpawnPoints.append( spawnpoint ) + } + + // Get titan spawns within the camp + foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) + { + vector campPos = info_target.GetOrigin() + vector spawnPos = spawnpoint.GetOrigin() + if ( Distance( campPos, spawnPos ) < campStruct.radius ) + campStruct.titanSpawnPoints.append( spawnpoint ) + } + + file.camps.append( campStruct ) + break; + } + } + } +} + +//////////////////////////////////////////// +///// GAMEMODE INITILAZE FUNCTIONS END ///// +//////////////////////////////////////////// + + + +///////////////////////////// +///// SCORING FUNCTIONS ///// +///////////////////////////// + +// TODO: Don't reward in postmatch +// TODO: Dropping a titan on a bounty with it's dome-shield still up rewards you the bonus, but +// it doesn't actually damage the bounty titan + +void function AT_ScoreEventsValueSetUp() +{ + ScoreEvent_SetEarnMeterValues( "KillTitan", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillAutoTitan", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "AttritionTitanKilled", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillPilot", 0.10, 0.10 ) + ScoreEvent_SetEarnMeterValues( "AttritionPilotKilled", 0.10, 0.10 ) + ScoreEvent_SetEarnMeterValues( "AttritionBossKilled", 0.10, 0.20 ) + ScoreEvent_SetEarnMeterValues( "AttritionGruntKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionSpectreKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionStalkerKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionSuperSpectreKilled", 0.10, 0.10, 0.5 ) + + // HACK + foreach ( string eventName in AT_ENABLE_SCOREEVENTS ) + ScoreEvent_Enable( GetScoreEvent( eventName ) ) + + foreach ( string eventName in AT_DISABLE_SCOREEVENTS ) + ScoreEvent_Disable( GetScoreEvent( eventName ) ) +} + +void function AT_PlayerOrNPCKilledScoreEvent( entity victim, entity attacker, var damageInfo ) +{ + if ( !IsValid( attacker ) ) + return + + // Suicide + if ( attacker == victim ) + { + if ( victim.IsPlayer() ) + AT_PlayerBonusLoss( victim, AT_GetPlayerBonusPoints( victim ) / 2 ) + + return + } + + // NPC is the attacker + if ( !attacker.IsPlayer() ) + { + if ( attacker.IsTitan() && IsValid( GetPetTitanOwner( attacker ) ) ) // Re-asign attacker + attacker = GetPetTitanOwner( attacker ) + else // NPC steals money from player, killing it will award the stolen bonus + normal reward + AT_NPCTryStealBonusPoints( attacker, victim ) + + return + } + + // Get event name + string eventName = GetAttritionScoreEventName( victim.GetClassName() ) + + if ( victim.IsTitan() ) // titan specific + eventName = GetAttritionScoreEventNameFromAI( victim ) + + if ( eventName == "" ) // no valid scoreEvent + return + + int scoreVal = ScoreEvent_GetPointValue( GetScoreEvent( eventName ) ) + + // pet titan check + if ( victim.IsTitan() && IsValid( GetPetTitanOwner( victim ) ) ) + { + if( GetPetTitanOwner( victim ) == attacker ) // Player ejected + return + + if( GetPetTitanOwner( victim ).IsPlayer() ) // Killed player npc titan + return + + scoreVal = ATTRITION_SCORE_TITAN_MIN + } + + // killed npc + if ( victim.IsNPC() ) + { + int bonusFromNPC = 0 + // If NPC was carrying a bonus award it to the attacker + if ( victim in file.npcStolenBonus ) + { + bonusFromNPC = file.npcStolenBonus[ victim ] + delete file.npcStolenBonus[ victim ] + } + AT_AddPlayerBonusPointsForEntityKilled( attacker, scoreVal, damageInfo, bonusFromNPC ) + AddPlayerScore( attacker, eventName ) // we add scoreEvent here, since basic score events has been overwrited by sh_gamemode_at.nut + // update score difference and scoreboard + AT_AddToPlayerTeamScore( attacker, scoreVal ) + } + + // bonus stealing check + if ( victim.IsPlayer() ) + AT_PlayerTryStealBonusPoints( attacker, victim, damageInfo ) +} + +bool function AT_NPCTryStealBonusPoints( entity attacker, entity victim ) +{ + // basic checks + if ( !attacker.IsNPC() ) + return false + + if ( !victim.IsPlayer() ) + return false + + int victimBonus = AT_GetPlayerBonusPoints( victim ) + int bonusToSteal = victimBonus / 2 // npc always steal half the bonus from player, no extra bonus for killing the player + if ( bonusToSteal == 0 ) // player has no bonus! + return false + + if ( !( attacker in file.npcStolenBonus ) ) // init + file.npcStolenBonus[ attacker ] <- 0 + + file.npcStolenBonus[ attacker ] += bonusToSteal + + AT_PlayerBonusLoss( victim, bonusToSteal ) // tell victim of bonus stolen + + if ( !( attacker in file.titanIsBountyBoss ) ) // if attacker npc is not a bounty titan, we make them highlighted + NPCBountyStolenHighlight( attacker ) + + return true +} + +void function NPCBountyStolenHighlight( entity npc ) +{ + Highlight_SetEnemyHighlight( npc, "enemy_boss_bounty" ) +} + +bool function AT_PlayerTryStealBonusPoints( entity attacker, entity victim, var damageInfo ) +{ + // basic checks + if ( !attacker.IsPlayer() ) + return false + + if ( !victim.IsPlayer() ) + return false + + int victimBonus = AT_GetPlayerBonusPoints( victim ) + + int minScoreCanSteal = ATTRITION_SCORE_PILOT_MIN + if ( victim.IsTitan() ) + minScoreCanSteal = ATTRITION_SCORE_TITAN_MIN + + int bonusToSteal = victimBonus / 2 + int attackerScore = bonusToSteal + bool realStealBonus = true + if ( bonusToSteal <= minScoreCanSteal ) // no enough bonus to steal + { + attackerScore = minScoreCanSteal // give attacker min bonus + realStealBonus = false // we don't do attacker steal events below, just half victim's bonus + } + + // servercallback + int victimEHandle = victim.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + // only do attacker events if victim has enough bonus to steal + if ( realStealBonus ) + { + Remote_CallFunction_NonReplay( + attacker, + "ServerCallback_AT_PlayerKillScorePopup", + bonusToSteal, // stolenScore + victimEHandle, // victimEHandle + damageOrigin.x, // x + damageOrigin.y, // y + damageOrigin.z // z + ) + } + else // otherwise we do a normal entity killed scoreEvent + { + AT_AddPlayerBonusPointsForEntityKilled( attacker, attackerScore, damageInfo ) + } + + // update score difference and scoreboard + AT_AddToPlayerTeamScore( attacker, minScoreCanSteal ) + + // steal bonus + // only do attacker events if victim has enough bonus to steal + if ( realStealBonus ) + { + AT_AddPlayerBonusPoints( attacker, bonusToSteal ) + AddPlayerScore( attacker, "AttritionBonusStolen" ) + } + + // tell victim of bonus stolen + AT_PlayerBonusLoss( victim, bonusToSteal ) + + return realStealBonus +} + +void function AT_PlayerBonusLoss( entity player, int bonusLoss ) +{ + AT_AddPlayerBonusPoints( player, -bonusLoss ) + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_ShowStolenBonus", + bonusLoss // stolenScore + ) +} + +// team score meter +void function AT_AddToPlayerTeamScore( entity player, int amount ) +{ + // do not award any score after the match is ended + if ( GetGameState() > eGameState.Playing ) + return + + // add to scoreboard + player.AddToPlayerGameStat( PGS_ASSAULT_SCORE, amount ) + + // Check score so we dont go over max + if ( GameRules_GetTeamScore(player.GetTeam()) + amount > GetScoreLimit_FromPlaylist() ) + { + amount = GetScoreLimit_FromPlaylist() - GameRules_GetTeamScore(player.GetTeam()) + } + + // update score difference + AddTeamScore( player.GetTeam(), amount ) +} + +// bonus points, players earn from killing +void function AT_AddPlayerBonusPoints( entity player, int amount ) +{ + // do not award any score after the match is ended + if ( GetGameState() > eGameState.Playing ) + return + + // add to scoreboard + player.AddToPlayerGameStat( PGS_SCORE, amount ) + AT_SetPlayerBonusPoints( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount ) +} + +int function AT_GetPlayerBonusPoints( entity player ) +{ + return player.GetPlayerNetInt( "AT_bonusPoints" ) + player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 +} + +void function AT_SetPlayerBonusPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_bonusPoints256", stacks ) + player.SetPlayerNetInt( "AT_bonusPoints", amount - stacks * 256 ) +} + +// total points, the value player actually uploaded to team score +void function AT_AddPlayerTotalPoints( entity player, int amount ) +{ + // update score difference and scoreboard, calling this function meaning player has deposited their bonus to team score + AT_AddToPlayerTeamScore( player, amount ) + AT_SetPlayerTotalPoints( player, player.GetPlayerNetInt( "AT_totalPoints" ) + ( player.GetPlayerNetInt( "AT_totalPoints256" ) * 256 ) + amount ) +} + +void function AT_SetPlayerTotalPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_totalPoints256", stacks ) + player.SetPlayerNetInt( "AT_totalPoints", amount - stacks * 256 ) +} + +// earn points, seems not used +void function AT_AddPlayerEarnedPoints( entity player, int amount ) +{ + AT_SetPlayerBonusPoints( player, player.GetPlayerNetInt( "AT_earnedPoints" ) + ( player.GetPlayerNetInt( "AT_earnedPoints256" ) * 256 ) + amount ) +} + +void function AT_SetPlayerEarnedPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_earnedPoints256", stacks ) + player.SetPlayerNetInt( "AT_earnedPoints", amount - stacks * 256 ) +} + +// damaging bounty +void function AT_AddPlayerBonusPointsForBossDamaged( entity player, entity victim, int amount, var damageInfo ) +{ + AT_AddPlayerBonusPoints( player, amount ) + // update score difference and scoreboard + AT_AddToPlayerTeamScore( player, amount ) + + // send servercallback for damaging + int bossEHandle = victim.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_BossDamageScorePopup", + amount, // damageScore + amount, // damageBonus + bossEHandle, // bossEHandle + damageOrigin.x, // x + damageOrigin.y, // y + damageOrigin.z // z + ) +} + +void function AT_AddPlayerBonusPointsForEntityKilled( entity player, int amount, var damageInfo, int extraBonus = 0 ) +{ + AT_AddPlayerBonusPoints( player, amount + extraBonus ) + + // send servercallback for damaging + int attackerEHandle = player.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_ShowATScorePopup", + attackerEHandle, // attackerEHandle + amount, // damageScore + amount + extraBonus, // damageBonus + damageOrigin.x, // damagePosX + damageOrigin.y, // damagePosX + damageOrigin.z, // damagePosX + 0 // damageType ( not used ) + ) +} + +///////////////////////////////// +///// SCORING FUNCTIONS END ///// +///////////////////////////////// + + + +////////////////////////////// +///// GAMELOOP FUNCTIONS ///// +////////////////////////////// + +void function AT_GameLoop_Threaded() +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // game end func + // TODO: Cant seem to be able to get this crash ??? + OnThreadEnd + ( + function() + { + // prevent crash before entity creation on map change + if ( GetGameState() >= eGameState.Prematch ) + { + SetGlobalNetBool( "preBankPhase", false ) + SetGlobalNetBool( "banksOpen", false ) + } + } + ) + + // Initial wait before first wave + wait AT_FIRST_WAVE_START_DELAY - AT_WAVE_TRANSITION_DELAY + + int lastWaveId = -1 + for ( int waveCount = 1; ; waveCount++ ) + { + wait AT_WAVE_TRANSITION_DELAY + + // cap to number of real waves + int waveId = ( waveCount - 1 ) / 2 + int waveCapAmount = 2 + waveId = int( min( waveId, GetWaveDataSize() - waveCapAmount ) ) + + // New wave dialogue + bool waveChanged = lastWaveId != waveId + if ( waveChanged ) + { + PlayFactionDialogueToTeam( "bh_newWave", TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_newWave", TEAM_MILITIA ) + } + else // same wave, second half + { + PlayFactionDialogueToTeam( "bh_incoming", TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_incoming", TEAM_MILITIA ) + } + + lastWaveId = waveId + + SetGlobalNetInt( "AT_currentWave", waveId ) + bool isBossWave = waveCount % 2 == 0 // even number waveCount means boss wave + + // announce the wave + foreach ( entity player in GetPlayerArray() ) + { + if ( isBossWave ) + { + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnounceBoss" ) + } + else + { + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_AnnouncePreParty", + 0.0, // endTime ( not used ) + waveId // waveNum + ) + } + } + + wait AT_WAVE_TRANSITION_DELAY + + // Run the wave + thread AT_CampSpawnThink( waveId, isBossWave ) + + if ( !isBossWave ) + { + svGlobal.levelEnt.WaitSignal( "ATAllCampsClean" ) // signaled when all camps cleaned in spawn functions + } + else + { + wait AT_BOUNTY_TITAN_CHECK_DELAY + // wait until all bounty titans killed + while ( IsAlive( GetGlobalNetEnt( "camp1Ent" ) ) || IsAlive( GetGlobalNetEnt( "camp2Ent" ) ) ) + WaitFrame() + } + + // wave end, prebank phase + svGlobal.levelEnt.Signal( "ATWaveEnd" ) // defensive fix, destroy existing campEnts + SetGlobalNetBool( "preBankPhase", true ) + + wait AT_WAVE_END_ANNOUNCEMENT_DELAY + + // announce wave end + foreach ( entity player in GetPlayerArray() ) + { + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_AnnounceWaveOver", + waveId, // waveNum ( not used ) + 0, // militiaDamageTotal ( not used ) + 0, // imcDamageTotal ( not used ) + 0, // milMVP ( not used ) + 0, // imcMVP ( not used ) + 0, // milMVPDamage ( not used ) + 0 // imcMVPDamage ( not used ) + ) + } + + wait AT_WAVE_TRANSITION_DELAY + + // banking phase + SetGlobalNetBool( "preBankPhase", false ) + SetGlobalNetTime( "AT_bankStartTime", Time() ) + SetGlobalNetTime( "AT_bankEndTime", Time() + AT_BANKS_OPEN_DURATION ) + SetGlobalNetBool( "banksOpen", true ) + + foreach ( entity player in GetPlayerArray() ) + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_BankOpen" ) + + foreach ( entity bank in file.banks ) + thread AT_BankActiveThink( bank ) + + + float endTime = Time() + AT_BANKS_OPEN_DURATION + bool forceCloseTriggered = false + // wait until no player is holding bonus, or max wait time + while ( Time() <= endTime ) + { + // If everyone has deposited their bonuses close the banks early + if ( !ATAnyPlayerHasBonus() && !forceCloseTriggered ) + { + forceCloseTriggered = true + endTime = Time() + AT_BANK_FORCE_CLOSE_DELAY + } + + WaitFrame() + } + + SetGlobalNetBool( "banksOpen", false ) + foreach ( entity player in GetPlayerArray() ) + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_BankClose" ) + } +} + +bool function ATAnyPlayerHasBonus() +{ + foreach ( entity player in GetPlayerArray() ) + { + if ( AT_GetPlayerBonusPoints( player ) ) + return true + } + return false +} + +////////////////////////////////// +///// GAMELOOP FUNCTIONS END ///// +////////////////////////////////// + + + +////////////////////////// +///// CAMP FUNCTIONS ///// +////////////////////////// + +void function AT_CampSpawnThink( int waveId, bool isBossWave ) +{ + AT_WaveData wave = GetWaveData( waveId ) + array< array > campSpawnData + + if ( isBossWave ) + campSpawnData = wave.bossSpawnData + else + campSpawnData = wave.spawnDataArrays + + array allCampsToUse + foreach ( AT_WaveOrigin campStruct in file.camps ) + { + if ( campStruct.phaseAllowed[ waveId ] ) + allCampsToUse.append( campStruct ) + } + + // HACK + // There's too many phase3 camps on exoplanet and rise, make sure we always have the correct count + int maxCampsForWave = waveId == 0 ? 1 : 2 + while( allCampsToUse.len() > maxCampsForWave ) + { + // Get the required number of camps + array tempCamps + for( int i = 0; i < maxCampsForWave; i++ ) + tempCamps.append( allCampsToUse[RandomInt( allCampsToUse.len() )] ) + + + // Check if they're intersecting, if they are, try again + bool intersecting = false + for( int i = 0; i < tempCamps.len(); i++ ) + { + AT_WaveOrigin campA = tempCamps[i] + for( int j = 0; j < tempCamps.len(); j++ ) + { + // Don't compare the same two camps + if( j == i ) + continue + + AT_WaveOrigin campB = tempCamps[j] + + if( Distance( campA.origin, campB.origin ) < campA.radius + campB.radius ) + intersecting = true + } + } + + if( !intersecting ) + allCampsToUse = tempCamps + + // If we ever get really unlucky just wait a frame + WaitFrame() + } + + foreach ( int spawnId, AT_WaveOrigin curCampData in allCampsToUse ) + { + array curSpawnData = campSpawnData[ spawnId ] + + int totalNPCsToSpawn = 0 + // initialise pending spawns and get total npcs + foreach ( AT_SpawnData spawnData in curSpawnData ) + { + spawnData.pendingSpawns = spawnData.totalToSpawn + // add to network variables + string npcNetVar = GetNPCNetVarName( spawnData.aitype, spawnId ) + SetGlobalNetInt( npcNetVar, spawnData.totalToSpawn ) + + totalNPCsToSpawn += spawnData.totalToSpawn + } + + if ( !isBossWave ) + { + // camp Ent, boss wave will use boss themselves as campEnt + string campEntVarName = "camp" + string( spawnId + 1 ) + "Ent" + bool waveNotActive = GetGlobalNetBool( "preBankPhase" ) || GetGlobalNetBool( "banksOpen" ) + if ( !IsValid( GetGlobalNetEnt( campEntVarName ) ) && !waveNotActive ) + SetGlobalNetEnt( campEntVarName, CreateCampTracker( curCampData, spawnId ) ) + + array minionSquadDatas + foreach ( AT_SpawnData data in curSpawnData ) + { + switch ( data.aitype ) + { + case "npc_soldier": + case "npc_spectre": + case "npc_stalker": + if ( !AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + minionSquadDatas.append( data ) + else + thread AT_DroppodSquadEvent_Single( curCampData, spawnId, data ) + break + + case "npc_super_spectre": + thread AT_ReaperEvent( curCampData, spawnId, data ) + break + } + } + + // minions squad spawn + if ( !AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + { + if ( minionSquadDatas.len() > 0 ) + thread AT_DroppodSquadEvent( curCampData, spawnId, minionSquadDatas ) + } + + // use campProgressThink for handling wave state + thread CampProgressThink( spawnId, totalNPCsToSpawn ) + } + else // bosswave spawn + { + foreach ( AT_SpawnData data in curSpawnData ) + { + if( data.aitype != "npc_titan" ) + continue + + thread AT_BountyTitanEvent( curCampData, spawnId, data ) + break + } + } + } +} + +void function CampProgressThink( int spawnId, int totalNPCsToSpawn ) +{ + string campLetter = GetCampLetter( spawnId ) + string campProgressName = campLetter + "campProgress" + string campEntVarName = "camp" + string( spawnId + 1 ) + "Ent" + + // initial wait + SetGlobalNetFloat( campProgressName, 1.0 ) + + // TODO: random wait, make this a constant ?? + wait 3.0 + + float cleanUpTime = -1.0 + + while ( true ) + { + int npcsLeft + // get all npcs might be in this camp + for ( int i = 0; i < 5; i++ ) + { + string netVarName = string( i + 1 ) + campLetter + "campCount" + int netVarValue = GetGlobalNetInt( netVarName ) + if ( netVarValue >= 0 ) // uninitialized network var starts from -1, avoid checking them + npcsLeft += netVarValue + } + + float campLeft = float( npcsLeft ) / float( totalNPCsToSpawn ) + SetGlobalNetFloat( campProgressName, campLeft ) -// IMPLEMENTATION NOTES: -// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives) -// in the interest of time, this script isn't gonna support any of that atm -// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead -// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience + if( npcsLeft <= AT_CAMP_BORED_NPCS_LEFT_TO_START_CLEANUP && cleanUpTime < 0.0 ) + { + cleanUpTime = Time() + AT_CAMP_BORED_CLEANUP_WAIT + print("Cleanup timer started!") + } -struct { - array campsToRegisterOnEntitiesDidLoad + if( Time() > cleanUpTime && cleanUpTime > 0.0 && spawnId in file.campScriptEntArrays ) + { + foreach( int handle in file.campScriptEntArrays[spawnId] ) + { + array entities = GetScriptManagedEntArray( handle ) + entities.removebyvalue( null ) + foreach ( entity ent in entities ) + { + if ( IsAlive( ent ) && ent.IsNPC() ) + { + printt( "Killing bored AI " + ent.GetClassName() + " at " + ent.GetOrigin() ) + ent.Die() + } + } + } + } - array banks - array camps - - table< int, table< string, int > > trackedCampNPCSpawns -} file + if ( campLeft <= 0.0 ) // camp wiped! + { + PlayFactionDialogueToTeam( "bh_cleared" + campLetter, TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_cleared" + campLetter, TEAM_MILITIA ) -void function GamemodeAt_Init() + entity campEnt = GetGlobalNetEnt( campEntVarName ) + if ( IsValid( campEnt ) ) + campEnt.Signal( "ATCampClean" ) // destroy the camp ent + + // check if both camps being destroyed + if ( !IsValid( GetGlobalNetEnt( "camp1Ent" ) ) && !IsValid( GetGlobalNetEnt( "camp2Ent" ) ) ) + svGlobal.levelEnt.Signal( "ATAllCampsClean" ) // end the wave + + return + } + + WaitFrame() + } +} + +// entity funcs +// camp +entity function CreateCampTracker( AT_WaveOrigin campData, int spawnId ) { - AddCallback_GameStateEnter( eGameState.Playing, RunATGame ) - - AddCallback_OnClientConnected( InitialiseATPlayer ) + // store data + vector campOrigin = campData.origin + float campRadius = campData.radius + float campHeight = campData.height + // add a minimap icon + entity mapIconEnt = CreateEntity( "prop_script" ) + DispatchSpawn( mapIconEnt ) + + mapIconEnt.SetOrigin( campOrigin ) + mapIconEnt.DisableHibernation() + SetTeam( mapIconEnt, AT_AI_TEAM ) + mapIconEnt.Minimap_AlwaysShow( TEAM_IMC, null ) + mapIconEnt.Minimap_AlwaysShow( TEAM_MILITIA, null ) + + mapIconEnt.Minimap_SetCustomState( GetCampMinimapState( spawnId ) ) + mapIconEnt.Minimap_SetAlignUpright( true ) + mapIconEnt.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + mapIconEnt.Minimap_SetObjectScale( campRadius / 16000.0 ) // proper icon on the map - AddSpawnCallbackEditorClass( "info_target", "info_attrition_bank", CreateATBank ) - AddSpawnCallbackEditorClass( "info_target", "info_attrition_camp", CreateATCamp ) - AddCallback_EntitiesDidLoad( CreateATCamps_Delayed ) + // attach a location tracker + entity tracker = GetAvailableLocationTracker() + tracker.SetOwner( mapIconEnt ) // needs a owner to show up + tracker.SetOrigin( campOrigin ) + SetLocationTrackerRadius( tracker, campRadius ) + SetLocationTrackerID( tracker, spawnId ) + DispatchSpawn( tracker ) + + thread TrackWaveEndForCampInfo( tracker, mapIconEnt ) + return tracker } -void function RateSpawnpoints_AT( int checkclass, array spawnpoints, int team, entity player ) +void function TrackWaveEndForCampInfo( entity tracker, entity mapIconEnt ) { - RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp + tracker.EndSignal( "OnDestroy" ) + tracker.EndSignal( "ATCampClean" ) + + OnThreadEnd + ( + function(): ( tracker, mapIconEnt ) + { + // camp cleaned, wave or game ended, destroy the camp info + if ( IsValid( tracker ) ) + tracker.Destroy() + + if ( IsValid( mapIconEnt ) ) + mapIconEnt.Destroy() + } + ) + + WaitSignal( svGlobal.levelEnt, "GameStateChanged", "ATWaveEnd" ) } -// world and player inits +string function GetCampLetter( int spawnId ) +{ + return spawnId == 0 ? "A" : "B" +} -void function InitialiseATPlayer( entity player ) +int function GetCampMinimapState( int id ) { - Remote_CallFunction_NonReplay( player, "ServerCallback_AT_OnPlayerConnected" ) + switch ( id ) + { + case 0: + return eMinimapObject_prop_script.AT_DROPZONE_A + case 1: + return eMinimapObject_prop_script.AT_DROPZONE_B + case 2: + return eMinimapObject_prop_script.AT_DROPZONE_C + } + + unreachable } -void function CreateATBank( entity spawnpoint ) +////////////////////////////// +///// CAMP FUNCTIONS END ///// +////////////////////////////// + + + +////////////////////////// +///// BANK FUNCTIONS ///// +////////////////////////// + +void function AT_BankActiveThink( entity bank ) { - entity bank = CreatePropDynamic( spawnpoint.GetModelName(), spawnpoint.GetOrigin(), spawnpoint.GetAngles(), SOLID_VPHYSICS ) - bank.SetScriptName( "AT_Bank" ) - - // create tracker ent - // we don't need to store these at all, client just needs to get them - DispatchSpawn( GetAvailableBankTracker( bank ) ) + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + bank.EndSignal( "OnDestroy" ) - thread PlayAnim( bank, "mh_inactive_idle" ) + // Banks closed + OnThreadEnd + ( + function(): ( bank ) + { + if ( IsValid( bank ) ) + { + // Update use prompt + if ( GetGameState() != eGameState.Playing ) + bank.UnsetUsable() + else + bank.SetUsePrompts( "#AT_USE_BANK_CLOSED", "#AT_USE_BANK_CLOSED" ) + + thread PlayAnim( bank, "mh_active_2_inactive" ) + FadeOutSoundOnEntity( bank, "Mobile_Hardpoint_Idle", 0.5 ) + bank.Minimap_Hide( TEAM_IMC, null ) + bank.Minimap_Hide( TEAM_MILITIA, null ) + } + } + ) + + // Update use prompt to usable + bank.SetUsable() + bank.SetUsePrompts( "#AT_USE_BANK", "#AT_USE_BANK_PC" ) + + thread PlayAnim( bank, "mh_inactive_2_active" ) + EmitSoundOnEntity( bank, "Mobile_Hardpoint_Idle" ) + + // Show minimap icon for bank + bank.Minimap_AlwaysShow( TEAM_IMC, null ) + bank.Minimap_AlwaysShow( TEAM_MILITIA, null ) + bank.Minimap_SetCustomState( eMinimapObject_prop_script.AT_BANK ) - file.banks.append( bank ) + // Wait for bank close or game end + while ( GetGlobalNetBool( "banksOpen" ) ) + WaitFrame() } -void function CreateATCamp( entity spawnpoint ) +function OnPlayerUseBank( bank, player ) { - // delay this so we don't do stuff before all spawns are initialised and that - file.campsToRegisterOnEntitiesDidLoad.append( spawnpoint ) -} + // Banks are always usable so that we can show the use prompt + // Only allow deposit when banks are open + if ( !GetGlobalNetBool( "banksOpen" ) ) + return -void function CreateATCamps_Delayed() -{ - // we delay registering camps until EntitiesDidLoad since they rely on spawnpoints and stuff, which might not all be ready in the creation callback - // unsure if this would be an issue in practice, but protecting against it in case it would be - foreach ( entity camp in file.campsToRegisterOnEntitiesDidLoad ) + expect entity( bank ) + expect entity( player ) + + // bank.SetUsableByGroup( "pilot" ) didn't seem to work so we just + // exit here if player is in a titan + if( player.IsTitan() ) + return + + // Player has no bonus, try to send a tip using SendHUDMessage + if ( AT_GetPlayerBonusPoints( player ) == 0 ) { - AT_WaveOrigin campStruct - campStruct.ent = camp - campStruct.origin = camp.GetOrigin() - campStruct.radius = expect string( camp.kv.radius ).tofloat() - campStruct.height = expect string( camp.kv.height ).tofloat() - - // assumes every info_attrition_camp will have all 9 phases, possibly not a good idea? - for ( int i = 0; i < 9; i++ ) - campStruct.phaseAllowed.append( expect string( camp.kv[ "phase_" + ( i + 1 ) ] ) == "1" ) - - // get droppod spawns - foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) - if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) - campStruct.dropPodSpawnPoints.append( spawnpoint ) - - foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) - if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) - campStruct.titanSpawnPoints.append( spawnpoint ) - - // todo: turret spawns someday maybe - - file.camps.append( campStruct ) + ATSendDepositTipToPlayer( player, "#AT_USE_BANK_NO_BONUS_HINT" ) + return } - - file.campsToRegisterOnEntitiesDidLoad.clear() -} -// scoring funcs + // Prevent more than one instance of this thread running + if ( !file.playerBankUploading[ player ] ) + thread PlayerUploadingBonus_Threaded( bank, player ) +} -// don't use this where possible as it doesn't set score and stuff -void function AT_SetPlayerCash( entity player, int amount ) +bool function ATSendDepositTipToPlayer( entity player, string message ) { - // split into stacks of 256 where necessary - int stacks = amount / 256 // automatically rounds down because int division + if ( Time() < file.playerHudMessageAllowedTime[ player ] ) + return false + + SendHudMessage( player, message, -1, 0.4, 255, 255, 255, 255, 0.5, 1.0, 0.5 ) + file.playerHudMessageAllowedTime[ player ] = Time() + AT_PLAYER_HUD_MESSAGE_COOLDOWN - player.SetPlayerNetInt( "AT_bonusPoints256", stacks ) - player.SetPlayerNetInt( "AT_bonusPoints", amount - stacks * 256 ) + return true } -void function AT_AddPlayerCash( entity player, int amount ) +struct AT_playerUploadStruct { - // update score difference - AddTeamScore( player.GetTeam(), amount / 2 ) - AT_SetPlayerCash( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount ) + bool uploadSuccess = false + int depositedPoints = 0 +} + +void function PlayerUploadingBonus_Threaded( entity bank, entity player ) +{ + bank.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) + + file.playerBankUploading[ player ] = true + + // this literally only exists because structs are passed by ref, + // and primitives like ints and bools are passed by val + // which meant that the OnThreadEnd was just getting 0 and false + AT_playerUploadStruct uploadInfo + + // Cleanup and call finish deposit func + OnThreadEnd + ( + function(): ( player, uploadInfo ) + { + if ( IsValid( player ) ) + { + file.playerBankUploading[ player ] = false + + // Clean up looping sound + StopSoundOnEntity( player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_1P" ) + StopSoundOnEntity( player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_3P" ) + + // Do medal event + // TODO: Check if vanilla actually do.s this every time you finish depositing??? + AddPlayerScore( player, "AttritionCashedBonus" ) + + // Do server callback + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_FinishDeposit", + uploadInfo.depositedPoints // deposit + ) + + player.SetPlayerNetBool( "AT_playerUploading", false ) + + if ( uploadInfo.uploadSuccess ) // Player deposited all remaining bonus + { + // Emit uploading successful sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Successful_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Successful_3P" ) + + // player is MVP + int ourScore = player.GetPlayerGameStat( PGS_ASSAULT_SCORE ) + bool isMVP = true + foreach(teamPlayer in GetPlayerArrayOfTeam(player.GetTeam())) + { + if (ourScore < teamPlayer.GetPlayerGameStat( PGS_ASSAULT_SCORE )) + { + isMVP = false + break + } + } + if (isMVP) + PlayFactionDialogueToPlayer( "bh_mvp", player ) + } + else // Player was killed or left the bank radius + { + // Emit uploading failed sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Unsuccessful_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Unsuccessful_3P" ) + } + } + } + ) + + // Uploading start sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_Start_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_Start_3P" ) + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_3P" ) + + player.SetPlayerNetBool( "AT_playerUploading", true ) + + // Upload bonus while the player is within range of the bank + while ( Distance( player.GetOrigin(), bank.GetOrigin() ) <= AT_BANK_DEPOSIT_RADIUS && GetGlobalNetBool( "banksOpen" ) ) + { + // Calling this moves the "Uploading..." graphic to the same place it is + // in vanilla + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_ShowRespawnBonusLoss" ) + + int bonusToUpload = int( min( AT_BANK_DEPOSIT_RATE, AT_GetPlayerBonusPoints( player ) ) ) + // No more bonus to upload, return + if ( bonusToUpload == 0 ) + { + uploadInfo.uploadSuccess = true + return + } + + // Remove bonus points and add them to total poins + AT_AddPlayerBonusPoints( player, -bonusToUpload ) + AT_AddPlayerTotalPoints( player, bonusToUpload ) + + uploadInfo.depositedPoints += bonusToUpload + WaitFrame() + } } -// run gamestate +////////////////////////////// +///// BANK FUNCTIONS END ///// +////////////////////////////// + -void function RunATGame() + +///////////////////////// +///// NPC FUNCTIONS ///// +///////////////////////// + +int function GetScriptManagedNPCArrayLength_Alive( int scriptManagerId ) { - thread RunATGame_Threaded() + array entities = GetScriptManagedEntArray( scriptManagerId ) + entities.removebyvalue( null ) + int npcsAlive = 0 + foreach ( entity ent in entities ) + { + if ( IsAlive( ent ) && ent.IsNPC() ) + npcsAlive += 1 + } + return npcsAlive } -void function RunATGame_Threaded() +void function AT_DroppodSquadEvent( AT_WaveOrigin campData, int spawnId, array minionDatas ) { svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + // create a script managed array for all handled minions + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] - OnThreadEnd( function() - { - SetGlobalNetBool( "banksOpen", false ) - }) - - wait WAVE_STATE_TRANSITION_TIME // initial wait before first wave - - for ( int waveCount = 1; ; waveCount++ ) + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = SQUAD_SIZE * AT_DROPPOD_SQUADS_ALLOWED_ON_FIELD + while ( true ) { - wait WAVE_STATE_TRANSITION_TIME - - // cap to number of real waves - int waveId = ( waveCount / 2 ) - // last wave is clearly unfinished so don't use, just cap to last actually used one - if ( waveId >= GetWaveDataSize() - 1 ) + foreach ( AT_SpawnData data in minionDatas ) { - waveId = GetWaveDataSize() - 2 - waveCount = waveId * 2 + string ent = data.aitype + waitthread AT_SpawnDroppodSquad( campData, spawnId, ent, eventManager ) + data.pendingSpawns -= SQUAD_SIZE + if ( data.pendingSpawns <= 0 ) // current spawn data has reached max spawn amount + minionDatas.removebyvalue( data ) // remove this data + if ( GetScriptManagedNPCArrayLength_Alive( eventManager ) >= totalAllowedOnField ) // we have enough npcs on field? + break // stop following spawning functions } - - SetGlobalNetInt( "AT_currentWave", waveId ) - bool isBossWave = waveCount / float( 2 ) > waveId // odd number waveCount means boss wave - - // announce the wave - foreach ( entity player in GetPlayerArray() ) + if ( minionDatas.len() == 0 ) // all spawn data has finished spawn + return + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField - SQUAD_SIZE ) // wait until we have lost more than 1 squad { - if ( isBossWave ) - Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnounceBoss" ) - else - Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnouncePreParty", 0.0, waveId ) + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) } - - wait WAVE_STATE_TRANSITION_TIME - - // run the wave - - AT_WaveData wave = GetWaveData( waveId ) - array< array > campSpawnData - - if ( isBossWave ) - campSpawnData = wave.bossSpawnData - else - campSpawnData = wave.spawnDataArrays - - // initialise pending spawns - foreach ( array< AT_SpawnData > campData in campSpawnData ) + } +} + +// for AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK, handles a single spawndata +void function AT_DroppodSquadEvent_Single( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // get ent and create a script managed array for current event + string ent = data.aitype + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = data.totalAllowedOnField // mostly 12 for grunts and spectres, too much! + // start spawner + while ( true ) + { + waitthread AT_SpawnDroppodSquad( campData, spawnId, ent, eventManager ) + data.pendingSpawns -= SQUAD_SIZE + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField - SQUAD_SIZE ) // wait until we have less npcs than allowed count { - foreach ( AT_SpawnData spawnData in campData ) - spawnData.pendingSpawns = spawnData.totalToSpawn - } - - // clear tracked spawns - file.trackedCampNPCSpawns = {} - while ( true ) - { - // if this is ever 0 by the end of this loop, wave is complete - int numActiveCampSpawners = 0 - - // iterate over camp data for wave - for ( int campIdx = 0; campIdx < campSpawnData.len() && campIdx < file.camps.len(); campIdx++ ) - { - if ( !( campIdx in file.trackedCampNPCSpawns ) ) - file.trackedCampNPCSpawns[ campIdx ] <- {} - - // iterate over ai spawn data for camp - foreach ( AT_SpawnData spawnData in campSpawnData[ campIdx ] ) - { - if ( !( spawnData.aitype in file.trackedCampNPCSpawns[ campIdx ] ) ) - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] <- 0 - - if ( spawnData.pendingSpawns > 0 || file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] > 0 ) - numActiveCampSpawners++ - - // try to spawn as many ai as we can, as long as the camp doesn't already have too many spawned - int spawnCount - for ( spawnCount = 0; spawnCount < spawnData.pendingSpawns && spawnCount < spawnData.totalAllowedOnField - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ]; ) - { - // not doing this in a generic way atm, but could be good for the future if we want to support more ai - switch ( spawnData.aitype ) - { - case "npc_soldier": - case "npc_spectre": - case "npc_stalker": - thread AT_SpawnDroppodSquad( campIdx, spawnData.aitype ) - spawnCount += 4 - break - - case "npc_super_spectre": - thread AT_SpawnReaper( campIdx ) - spawnCount += 1 - break - - case "npc_titan": - thread AT_SpawnBountyTitan( campIdx ) - spawnCount += 1 - break - - default: - print( "BOUNTY HUNT: Tried to spawn unsupported ai of type \"" + "\" at camp " + campIdx ) - } - } - - // track spawns - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] += spawnCount - spawnData.pendingSpawns -= spawnCount - } - } - - if ( numActiveCampSpawners == 0 ) - break - - wait 0.5 + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) } - - wait WAVE_STATE_TRANSITION_TIME - - // banking phase } } -// entity funcs - -void function AT_SpawnDroppodSquad( int camp, string aiType ) +void function AT_SpawnDroppodSquad( AT_WaveOrigin campData, int spawnId, string aiType, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.dropPodSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].dropPodSpawnPoints.getrandom() + spawnpoint = campData.dropPodSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) - AiGameModes_SpawnDropPod( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, aiType, void function( array guys ) : ( camp, aiType ) - { - AT_HandleSquadSpawn( guys, camp, aiType ) - }) + AiGameModes_SpawnDropPod( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + aiType, + // squad handler + void function( array guys ) : ( campData, spawnId, aiType, scriptManagerId ) + { + AT_HandleSquadSpawn( guys, campData, spawnId, aiType, scriptManagerId ) + }, + eDropPodFlag.DISSOLVE_AFTER_DISEMBARKS + ) } -void function AT_HandleSquadSpawn( array guys, int camp, string aiType ) +void function AT_HandleSquadSpawn( array guys, AT_WaveOrigin campData, int spawnId, string aiType, int scriptManagerId ) { foreach ( entity guy in guys ) { - guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) - - // untrack them on death - thread AT_WaitToUntrackNPC( guy, camp, aiType ) + // TODO: NPCs still seem to go outside their camp ??? + //guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) + + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, guy ) + thread AT_TrackNPCLifeTime( guy, spawnId, aiType ) + + thread AT_ForceAssaultAroundCamp( guy, campData ) + } +} + +void function AT_ForceAssaultAroundCamp( entity guy, AT_WaveOrigin campData ) +{ + guy.EndSignal( "OnDestroy" ) + guy.EndSignal( "OnDeath" ) + + // goal check + vector ornull goalPos = NavMesh_ClampPointForAI(campData.origin, guy) + goalPos = goalPos == null ? campData.origin : goalPos + expect vector(goalPos) + + float goalRadius = campData.radius / 4 + float guyGoalRadius = guy.GetMinGoalRadius() + if ( guyGoalRadius > goalRadius ) // this npc cannot use forced goal radius? + goalRadius = guyGoalRadius + + while( true ) + { + guy.AssaultPoint( goalPos ) + guy.AssaultSetGoalRadius( goalRadius ) + guy.AssaultSetFightRadius( 0 ) + guy.AssaultSetArrivalTolerance( int(goalRadius) ) + + wait RandomFloatRange( 1, 5 ) + } +} + +void function AT_ReaperEvent( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // create a script managed array for current event + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = 1 // 1 allowed at the same time for heavy armor units + if ( AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + totalAllowedOnField = data.totalAllowedOnField + + while ( true ) + { + waitthread AT_SpawnReaper( campData, spawnId, eventManager ) + data.pendingSpawns -= 1 + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } } } -void function AT_SpawnReaper( int camp ) +void function AT_SpawnReaper( AT_WaveOrigin campData, int spawnId, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.dropPodSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + spawnpoint = campData.dropPodSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) - AiGameModes_SpawnReaper( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, "npc_super_spectre",void function( entity reaper ) : ( camp ) + AiGameModes_SpawnReaper( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + "npc_super_spectre_aitdm", + // reaper handler + void function( entity reaper ) : ( campData, spawnId, scriptManagerId ) + { + AT_HandleReaperSpawn( reaper, campData, spawnId, scriptManagerId ) + } + ) +} + +void function AT_HandleReaperSpawn( entity reaper, AT_WaveOrigin campData, int spawnId, int scriptManagerId ) +{ + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, reaper ) + thread AT_TrackNPCLifeTime( reaper, spawnId, "npc_super_spectre" ) + + thread AT_ForceAssaultAroundCamp( reaper, campData ) +} + +void function AT_BountyTitanEvent( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // create a script managed array for current event + int eventManager = CreateScriptManagedEntArray() + + int totalAllowedOnField = 1 // 1 allowed at the same time for heavy armor units + if ( AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + totalAllowedOnField = data.totalAllowedOnField + while ( true ) { - thread AT_WaitToUntrackNPC( reaper, camp, "npc_super_spectre" ) - }) + waitthread AT_SpawnBountyTitan( campData, spawnId, eventManager ) + data.pendingSpawns -= 1 + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } } -void function AT_SpawnBountyTitan( int camp ) +void function AT_SpawnBountyTitan( AT_WaveOrigin campData, int spawnId, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.titanSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + spawnpoint = campData.titanSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) @@ -320,57 +1624,178 @@ void function AT_SpawnBountyTitan( int camp ) int bountyID = 0 try { - bountyID = ReserveBossID( VALID_BOUNTY_TITAN_SETTINGS.getrandom() ) + bountyID = ReserveBossID( AT_BOUNTY_TITANS_AI_SETTINGS.getrandom() ) } catch ( ex ) {} // if we go above the expected wave count that vanilla supports, there's basically no way to ensure that this func won't error, so default 0 after that point string aisettings = GetTypeFromBossID( bountyID ) string titanClass = expect string( Dev_GetAISettingByKeyField_Global( aisettings, "npc_titan_player_settings" ) ) + AiGameModes_SpawnTitan( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + titanClass, + aisettings, + // titan handler + void function( entity titan ) : ( campData, spawnId, bountyID, scriptManagerId ) + { + AT_HandleBossTitanSpawn( titan, campData, spawnId, bountyID, scriptManagerId ) + } + ) +} + +void function AT_HandleBossTitanSpawn( entity titan, AT_WaveOrigin campData, int spawnId, int bountyID, int scriptManagerId ) +{ + // set the bounty to be campEnt, for client tracking + SetGlobalNetEnt( "camp" + string( spawnId + 1 ) + "Ent", titan ) + // set up health + titan.SetMaxHealth( titan.GetMaxHealth() * AT_BOUNTY_TITAN_HEALTH_MULTIPLIER ) + titan.SetHealth( titan.GetMaxHealth() ) + // make minimap always show them and highlight them + titan.Minimap_AlwaysShow( TEAM_IMC, null ) + titan.Minimap_AlwaysShow( TEAM_MILITIA, null ) + thread BountyBossHighlightThink( titan ) + + // set up titan-specific death callbacks, mark it as bounty boss + file.titanIsBountyBoss[ titan ] <- true + file.bountyTitanRewards[ titan ] <- ATTRITION_SCORE_BOSS_DAMAGE + AddEntityCallback_OnPostDamaged( titan, OnBountyTitanPostDamage ) + AddEntityCallback_OnKilled( titan, OnBountyTitanKilled ) - AiGameModes_SpawnTitan( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, titanClass, aisettings, void function( entity titan ) : ( camp, bountyID ) + titan.GetTitanSoul().soul.skipDoomState = true + // i feel like this should be localised, but there's nothing for it in r1_english? + titan.SetTitle( GetNameFromBossID( bountyID ) ) + + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, titan ) + thread AT_TrackNPCLifeTime( titan, spawnId, "npc_titan" ) +} + +void function BountyBossHighlightThink( entity titan ) +{ + titan.EndSignal( "OnDestroy" ) + titan.EndSignal( "OnDeath" ) + + while ( true ) { - // set up titan-specific death/damage callbacks - AddEntityCallback_OnDamaged( titan, OnBountyDamaged) - AddEntityCallback_OnKilled( titan, OnBountyKilled ) - - titan.GetTitanSoul().soul.skipDoomState = true - // i feel like this should be localised, but there's nothing for it in r1_english? - titan.SetTitle( GetNameFromBossID( bountyID ) ) - thread AT_WaitToUntrackNPC( titan, camp, "npc_titan" ) - } ) + Highlight_SetEnemyHighlight( titan, "enemy_boss_bounty" ) + titan.WaitSignal( "StopPhaseShift" ) // prevent phase shift mess up highlights + } } -// Tracked entities will require their own "wallet" -// for titans it should be used for rounding error compenstation -// for infantry it sould be used to store money if the npc kills a player -void function OnBountyDamaged( entity titan, var damageInfo ) +void function OnBountyTitanPostDamage( entity titan, var damageInfo ) { entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !IsValid( attacker ) ) // delayed by projectile shots + return + // damaged by npc or something? if ( !attacker.IsPlayer() ) - attacker = GetLatestAssistingPlayerInfo( titan ).player - - if ( IsValid( attacker ) && attacker.IsPlayer() ) { - int reward = int ( BOUNTY_TITAN_DAMAGE_POOL * DamageInfo_GetDamage( damageInfo ) / titan.GetMaxHealth() ) - printt ( titan.GetMaxHealth(), DamageInfo_GetDamage( damageInfo ) ) - - AT_AddPlayerCash( attacker, reward ) + attacker = GetBountyBossDamageOwner( attacker, titan ) + if ( !IsValid( attacker ) || !attacker.IsPlayer() ) + return } + + int rewardSegment = ATTRITION_SCORE_BOSS_DAMAGE + int healthSegment = titan.GetMaxHealth() / rewardSegment + + // sometimes damage is not enough to add 1 point, we save the damage for player's next attack + if ( !( titan in file.playerSavedBountyDamage[ attacker ] ) ) + file.playerSavedBountyDamage[ attacker ][ titan ] <- 0 + + file.playerSavedBountyDamage[ attacker ][ titan ] += int( DamageInfo_GetDamage( damageInfo ) ) + if ( file.playerSavedBountyDamage[ attacker ][ titan ] < healthSegment ) + return // they can't earn reward from this shot + + int damageSegment = file.playerSavedBountyDamage[ attacker ][ titan ] / healthSegment + int savedDamageLeft = file.playerSavedBountyDamage[ attacker ][ titan ] % healthSegment + file.playerSavedBountyDamage[ attacker ][ titan ] = savedDamageLeft + + float damageFrac = float( damageSegment ) / rewardSegment + int rewardLeft = file.bountyTitanRewards[ titan ] + int reward = int( ATTRITION_SCORE_BOSS_DAMAGE * damageFrac ) + if ( reward >= rewardLeft ) // overloaded shot? + reward = rewardLeft + file.bountyTitanRewards[ titan ] -= reward + + if ( reward > 0 ) + AT_AddPlayerBonusPointsForBossDamaged( attacker, titan, reward, damageInfo ) } -void function OnBountyKilled( entity titan, var damageInfo ) +void function OnBountyTitanKilled( entity titan, var damageInfo ) { entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !IsValid( attacker ) ) // delayed by projectile shots + return + // damaged by npc or something? if ( !attacker.IsPlayer() ) - attacker = GetLatestAssistingPlayerInfo( titan ).player + { + attacker = GetBountyBossDamageOwner( attacker, titan ) + if ( !IsValid( attacker ) || !attacker.IsPlayer() ) + return + } + + // add all remaining reward to attacker + // bounty killed bonus handled by AT_PlayerOrNPCKilledScoreEvent() + int rewardLeft = file.bountyTitanRewards[ titan ] + delete file.bountyTitanRewards[ titan ] + if ( rewardLeft > 0 ) + AT_AddPlayerBonusPointsForBossDamaged( attacker, titan, rewardLeft, damageInfo ) + + // remove this bounty's damage saver + foreach ( entity player in GetPlayerArray() ) + { + if ( titan in file.playerSavedBountyDamage[ player ] ) + delete file.playerSavedBountyDamage[ player ][ titan ] + } + + // faction dialogue + int team = attacker.GetTeam() + PlayFactionDialogueToPlayer( "bh_playerKilledBounty", attacker ) + PlayFactionDialogueToTeamExceptPlayer( "bh_bountyClaimedByFriendly", team, attacker ) + PlayFactionDialogueToTeam( "bh_bountyClaimedByEnemy", GetOtherTeam( team ) ) +} + +entity function GetBountyBossDamageOwner( entity attacker, entity titan ) +{ + if ( attacker.IsPlayer() ) // already a player + return attacker - if ( IsValid( attacker ) && attacker.IsPlayer() ) - AT_AddPlayerCash( attacker, BOUNTY_TITAN_KILL_REWARD ) + if ( attacker.IsTitan() ) // attacker is a npc titan + { + // try to find it's pet titan owner + if ( IsValid( GetPetTitanOwner( attacker ) ) ) + return GetPetTitanOwner( attacker ) + } + + // other damages or non-owner npcs, not sure how it happens, just use this titan's last attacker + return GetLatestAssistingPlayerInfo( titan ).player } -void function AT_WaitToUntrackNPC( entity guy, int camp, string aiType ) +void function AT_TrackNPCLifeTime( entity guy, int spawnId, string aiType ) { guy.WaitSignal( "OnDeath", "OnDestroy" ) - file.trackedCampNPCSpawns[ camp ][ aiType ]-- + + string npcNetVar = GetNPCNetVarName( aiType, spawnId ) + SetGlobalNetInt( npcNetVar, GetGlobalNetInt( npcNetVar ) - 1 ) +} + + +// network var +string function GetNPCNetVarName( string className, int spawnId ) +{ + string npcId = string( GetAiTypeInt( className ) + 1 ) + string campLetter = GetCampLetter( spawnId ) + if ( npcId == "0" ) // cannot find this ai support! + { + if ( className == "npc_super_spectre" ) // stupid, reapers are not handled by GetAiTypeInt(), but it must be 4 + return "4" + campLetter + "campCount" + return "" + } + return npcId + campLetter + "campCount" } + +///////////////////////////// +///// NPC FUNCTIONS END ///// +/////////////////////////////