diff --git a/.github/nativefuncs.json b/.github/nativefuncs.json index 110429031..10ceebd5d 100644 --- a/.github/nativefuncs.json +++ b/.github/nativefuncs.json @@ -7,10 +7,10 @@ "argTypes":"" }, { - "name":"NSIsModEnabled", + "name":"NSGetModsInformation", "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" + "returnTypeString":"array", + "argTypes":"" }, { "name":"NSSetModEnabled", @@ -18,48 +18,6 @@ "returnTypeString":"void", "argTypes":"string modName, bool enabled" }, - { - "name":"NSIsModRemote", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModDescriptionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModVersionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModDownloadLinkByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModLoadPriority", - "helpText":"", - "returnTypeString":"int", - "argTypes":"string modName" - }, - { - "name":"NSIsModRequiredOnClient", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModConvarsByModName", - "helpText":"", - "returnTypeString":"array", - "argTypes":"string modName" - }, { "name":"DecodeJSON", "helpText":"converts a json string to a squirrel table", @@ -260,60 +218,12 @@ "returnTypeString":"array", "argTypes":"" }, - { - "name":"NSIsModEnabled", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, { "name":"NSSetModEnabled", "helpText":"", "returnTypeString":"void", "argTypes":"string modName, bool enabled" }, - { - "name":"NSIsModRemote", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModDescriptionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModVersionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModDownloadLinkByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModLoadPriority", - "helpText":"", - "returnTypeString":"int", - "argTypes":"string modName" - }, - { - "name":"NSIsModRequiredOnClient", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModConvarsByModName", - "helpText":"", - "returnTypeString":"array", - "argTypes":"string modName" - }, { "name":"DecodeJSON", "helpText":"converts a json string to a squirrel table", @@ -467,58 +377,22 @@ "argTypes":"" }, { - "name":"NSIsModEnabled", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSSetModEnabled", + "name":"NSGetModsInformation", "helpText":"", - "returnTypeString":"void", - "argTypes":"string modName, bool enabled" - }, - { - "name":"NSIsModRemote", - "helpText":"", - "returnTypeString":"bool", - "argTypes":"string modName" - }, - { - "name":"NSGetModDescriptionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModVersionByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModDownloadLinkByModName", - "helpText":"", - "returnTypeString":"string", - "argTypes":"string modName" - }, - { - "name":"NSGetModLoadPriority", - "helpText":"", - "returnTypeString":"int", - "argTypes":"string modName" + "returnTypeString":"array", + "argTypes":"" }, { - "name":"NSIsModRequiredOnClient", + "name":"NSGetModInformation", "helpText":"", - "returnTypeString":"bool", + "returnTypeString":"array", "argTypes":"string modName" }, { - "name":"NSGetModConvarsByModName", + "name":"NSSetModEnabled", "helpText":"", - "returnTypeString":"array", - "argTypes":"string modName" + "returnTypeString":"void", + "argTypes":"string modName, bool enabled" }, { "name": "NSFetchVerifiedModsManifesto", @@ -540,6 +414,12 @@ "returnTypeString": "void", "argTypes": "string name, string version" }, + { + "name": "NSCancelModDownload", + "helpText": "prevents installation of the mod currently being installed", + "returnTypeString": "void", + "argTypes": "" + }, { "name": "NSGetModInstallState", "helpText": "get status of the mod currently being installed", diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml index 8803f4f4c..c0fe8687a 100644 --- a/.github/workflows/compile-check.yml +++ b/.github/workflows/compile-check.yml @@ -17,6 +17,14 @@ jobs: with: mods-directory: "${{ github.workspace }}/mods" native-json: "${{ github.workspace }}/mods/.github/nativefuncs.json" + vanilla: false + + - name: Compile Scripts (Vanilla) + uses: ASpoonPlaysGames/squirrel-re-compiler@v3 + with: + mods-directory: "${{ github.workspace }}/mods" + native-json: "${{ github.workspace }}/mods/.github/nativefuncs.json" + vanilla: true # It's important that scripts compile when Northstar.Custom isn't enabled/installed, so run again without it - name: Remove Northstar.Custom @@ -28,3 +36,11 @@ jobs: with: mods-directory: "${{ github.workspace }}/mods" native-json: "${{ github.workspace }}/mods/.github/nativefuncs.json" + vanilla: false + + - name: Compile Scripts (Vanilla, No Northstar.Custom) + uses: ASpoonPlaysGames/squirrel-re-compiler@v3 + with: + mods-directory: "${{ github.workspace }}/mods" + native-json: "${{ github.workspace }}/mods/.github/nativefuncs.json" + vanilla: true diff --git a/Northstar.Client/mod.json b/Northstar.Client/mod.json index 44937a2b0..0d0cfc169 100644 --- a/Northstar.Client/mod.json +++ b/Northstar.Client/mod.json @@ -46,6 +46,10 @@ "Name": "modlist_reverse", "DefaultValue": "0", "Flags": "ARCHIVE_PLAYERPROFILE" + }, + { + "Name": "modemenu_mode_filter", + "DefaultValue": "-1" } ], "Scripts": [ diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt index 29d7cddf8..76e4fce1f 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt @@ -312,6 +312,12 @@ Press Yes if you agree to this. This choice can be changed in the mods menu at a // In-game chat "HUD_CHAT_WHISPER_PREFIX" "[WHISPER]" "HUD_CHAT_SERVER_PREFIX" "[SERVER]" + + // Team Switching + "TEAMSWITCH_GAMEMODE" "Gamemode does not allow Team Switching" + "TEAMSWITCH_BUFFER" "Team Switching is on Cooldown" + "TEAMSWITCH_GAMEPLAY" "Team change not allowed outside playing phase" + "TEAMSWITCH_DISABLED" "Current gamemode doesn't support team change" "NO_GAMESERVER_RESPONSE" "Couldn't reach game server" "BAD_GAMESERVER_RESPONSE" "Game server gave an invalid response" @@ -324,6 +330,18 @@ Press Yes if you agree to this. This choice can be changed in the mods menu at a "JSON_PARSE_ERROR" "Error parsing json response" "UNSUPPORTED_VERSION" "The version you are using is no longer supported" + // Mode menu + "MODE_MENU_PVPVE" "PvPvE" + "MODE_MENU_PVE" "PvE" + "MODE_MENU_PVP" "PvP" + "MODE_MENU_FFA" "FFA" + "MODE_MENU_TITAN_ONLY" "Titan Only" + "MODE_MENU_OTHER" "Other" + "MODE_MENU_CUSTOM" "Custom" + "MODE_MENU_ALL" "All" + "MODE_MENU_UNKNOWN" "Unknown" + "MODE_MENU_SWITCH" "Filter" + "AUTHENTICATION_FAILED_HEADER" "Authentication Failed" "AUTHENTICATION_FAILED_BODY" "Failed to authenticate with Atlas!" "AUTHENTICATION_FAILED_ERROR_CODE" "Error code: ^DB6F2C00%s1^" diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_french.txt b/Northstar.Client/mod/resource/northstar_client_localisation_french.txt index 9444a39e3..38d601b0c 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_french.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_french.txt @@ -367,7 +367,7 @@ Choisissez Oui si vous êtes d'accord. Ce choix peut être modifié à tout inst "DOWNLOADING_MOD_TEXT" "Téléchargement de %s1 v%s2..." "WRONG_MOD_VERSION" "Le serveur requiert la version v%s2 du mod \"%s1\" (vous avez la version v%s3)" "DOWNLOADING_MOD_TEXT_W_PROGRESS" "Téléchargement de %s1 v%s2...\n(%s3/%s4 Mo)" - "CHECKSUMING_TITLE" "Vérification de la somme de contrôle du mod" + "CHECKSUMING_TITLE" "Vérification de l'intégrité du mod" "CHECKSUMING_TEXT" "Vérification du contenu de %s1 v%s2..." "EXTRACTING_MOD_TEXT" "Extraction de %s1 v%s2...\n(%s3/%s4 Mo)" "FAILED_DOWNLOADING" "Echec du téléchargement du mod" @@ -379,5 +379,15 @@ Choisissez Oui si vous êtes d'accord. Ce choix peut être modifié à tout inst "MOD_FETCHING_FAILED_GENERAL" "L'extraction du mod a échoué. Consultez le journal pour plus d'informations." "MANIFESTO_FETCHING_TITLE" "Préparation du téléchargement du mod" "MANIFESTO_FETCHING_TEXT" "Récupération de la liste des mods vérifiés..." + "MODE_MENU_PVPVE" "JcJcE" + "MODE_MENU_PVE" "JcE" + "MODE_MENU_PVP" "JcJ" + "MODE_MENU_FFA" "Mêlée générale" + "MODE_MENU_OTHER" "Autre" + "MODE_MENU_CUSTOM" "Personnalisé" + "MODE_MENU_ALL" "Tout" + "MODE_MENU_UNKNOWN" "Inconnu" + "MODE_MENU_SWITCH" "Filtre" + "MODE_MENU_TITAN_ONLY" "Titan Uniquement" } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt b/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt index 089edf35b..eebdcb714 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_italian.txt @@ -20,7 +20,7 @@ Premi Sì se sei d'accordo. Questa scelta può essere modificata in qualsiasi mo "DIALOG_AUTHENTICATING_MASTERSERVER", "Autenticazione Sul Master Server in corso" "AUTHENTICATIONAGREEMENT_NO", "Hai Scelto di non autenticarti con Northstar. Puoi vedere l'Accordo nel Menu delle Mods" - "MENU_TITLE_SERVER_BROWSER" "Server Browser" + "MENU_TITLE_SERVER_BROWSER" "Lista dei Server" "NS_SERVERBROWSER_NOSERVERS" "Nessun server trovato" "NS_SERVERBROWSER_UNKNOWNMODE", "Modalità Sconosciuta" "NS_SERVERBROWSER_WAITINGFORSERVERS" "In attesa dei server..." diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt b/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt index 798d603e0..47507c001 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_japanese.txt @@ -404,6 +404,7 @@ "PROGRESSION_ANNOUNCEMENT_BODY" "^CCCC0000進行システムが開放されました!^\n\nNorthstarはバニラの進行システム、つまり武器、スキン、タイタン等をレベルアップやチャレンジ達成で解除する機能をサポートしています。\n\nこの進行システムは、ロビー画面下のボタンから有効にできます。\n\nこの設定はいつでも変更可能です。" "MOD_NOT_VERIFIED" "(Modが検証されていないため、自動ダウンロードできませんでした)" "WRONG_MOD_VERSION" "サーバーはMod\"%s1\"v%s2 を要求していますが、あなたのModバージョンは%s3です" + "MODE_MENU_PVPVE" "PvPvE" // Translation done by Zetryox and CYakigasi } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_portuguese.txt b/Northstar.Client/mod/resource/northstar_client_localisation_portuguese.txt index 75a5faad6..79f3272de 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_portuguese.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_portuguese.txt @@ -304,8 +304,8 @@ Clique em Sim se você concorda. Esta escolha pode ser alterada a qualquer momen "HIDE_LOCKED" "Esconder desativados" // In-game chat - "HUD_CHAT_WHISPER_PREFIX" "[WHISPER]" - "HUD_CHAT_SERVER_PREFIX" "[SERVER]" + "HUD_CHAT_WHISPER_PREFIX" "[SUSSURRO]" + "HUD_CHAT_SERVER_PREFIX" "[SERVIDOR]" "ONLY_HOST_MATCH_SETTINGS" "Somente o Host pode mudar as configurações da Partida Privada" "ONLY_HOST_CAN_START_MATCH" "Somente o Host pode Iniciar a Partida" "LOG_UNKNOWN_CLIENTCOMMANDS" "Registrar Comandos desconhecidos de Clientes" @@ -355,8 +355,8 @@ Clique em Sim se você concorda. Esta escolha pode ser alterada a qualquer momen "MOD_DL_DISABLED" "(download automático de mod está desabilitado)" "DOWNLOADING_MOD_TITLE" "Baixando mod" "DOWNLOADING_MOD_TEXT_W_PROGRESS" "Baixando %s1 v%s2...\n(%s3/%s4 MB)" - "CHECKSUMING_TITLE" "Verificando o mod" - "CHECKSUMING_TEXT" "Verificando conteúdo de %s1 v%s2..." + "CHECKSUMING_TITLE" "Verificando a integridade do mod" + "CHECKSUMING_TEXT" "Validando arquivos de %s1 v%s2..." "EXTRACTING_MOD_TITLE" "Extraíndo mod (%s1%)" "MOD_REQUIRED_WARNING" " Este mod pode ser desativado quando entrar em um servidor" "AUTHENTICATION_FAILED_HEADER" "Autenticação Falhou" @@ -376,5 +376,15 @@ Clique em Sim se você concorda. Esta escolha pode ser alterada a qualquer momen "MOD_FETCHING_FAILED_GENERAL" "Extração do mod falhou. Verifique os logs para mais detalhes." "MANIFESTO_FETCHING_TEXT" "Retornando a lista de mods verificados..." "MANIFESTO_FETCHING_TITLE" "Preparando o download do mod" + "MODE_MENU_PVPVE" "JcJcA" + "MODE_MENU_PVE" "JcA" + "MODE_MENU_PVP" "JcJ" + "MODE_MENU_FFA" "TcT" + "MODE_MENU_OTHER" "Outros" + "MODE_MENU_CUSTOM" "Personalizado" + "MODE_MENU_ALL" "Todos" + "MODE_MENU_UNKNOWN" "Desconhecido" + "MODE_MENU_SWITCH" "Filtrar" + "MODE_MENU_TITAN_ONLY" "Somente Titãs" } } diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt index 91a0dedee..7742e768b 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt @@ -32,7 +32,7 @@ "PRIVATE_MATCH_SINGLEPLAYER_LEVEL" "%s1 (Одиночная игра)" // fra hint for private match menu, because fra only has PL_fra_desc in vanilla - "PL_fra_hint" "Ты один. Уничтожь противников, чтобы победить. Собери 3 батареи для вызова титана." + "PL_fra_hint" "Все против всех. Уничтожай противников, чтобы победить. Собери 3 батареи для вызова титана." // mode settings "MODE_SETTING_CATEGORY_PILOT" "Пилот" @@ -41,7 +41,7 @@ "MODE_SETTING_CATEGORY_MATCH" "Матч" "classic_mp" "Классический мультиплеер" - "run_epilogue" "Показывать эпилог" + "run_epilogue" "Запускать эпилог" "scorelimit" "Лимит очков" "roundscorelimit" "Лимит очков (по раундам)" "timelimit" "Лимит времени" @@ -86,18 +86,18 @@ "PL_gg_abbr" "ГВ" "GAMEMODE_GG" "Гонка вооружений" - "PL_tt" "Разборки Титанов" - "PL_tt_lobby" "Лобби разборок титанов" - "PL_tt_desc" "Зарабатывайте очки, пока вы находитесь в своём титане. Уничтожьте титана чтобы получить своего." - "PL_tt_hint" "Зарабатывайте очки, пока вы находитесь в своём титане. Уничтожьте титана чтобы получить своего." - "PL_tt_abbr" "TT" - "GAMEMODE_TT" "Разборки титанов" + "PL_tt" "Салочки" + "PL_tt_lobby" "Лобби салочек" + "PL_tt_desc" "Зарабатывайте очки будучи в титане. Уничтожьте титана чтобы получить собственного." + "PL_tt_hint" "Зарабатывайте очки будучи в титане. Уничтожьте титана чтобы получить собственного." + "PL_tt_abbr" "СЛ" + "GAMEMODE_TT" "Салочки" "PL_inf" "Заражение" "PL_inf_lobby" "Лобби Заражения" "PL_inf_desc" "Переживите инфекцию. Выжившие становятся заражёнными при убийстве." "PL_inf_hint" "Переживите инфекцию. Выжившие становятся заражёнными при убийстве." - "PL_inf_abbr" "INF" + "PL_inf_abbr" "ЗРЖ" "GAMEMODE_INF" "Заражение" "INFECTION_YOU_ARE_INFECTED" "Вас заразили!" "INFECTION_KILL_SURVIVORS" "Заразите всех оставшихся выживших." @@ -111,7 +111,7 @@ "PL_hs_lobby" "Лобби пряток" "PL_hs_desc" "Игра со стандартными правилами пряток." "PL_hs_hint" "Игра со стандартными правилами пряток." - "PL_hs_abbr" "HS" + "PL_hs_abbr" "ПРЯТ" "GAMEMODE_hs" "Прятки" "HIDEANDSEEK_YOU_ARE_SEEKER" "ВЫ ИЩЕТЕ" "HIDEANDSEEK_SEEKER_DESC" "Найдите прячущихся и ударьте их.\nВы появитесь через %s1 секунд(у)" @@ -250,7 +250,7 @@ "UNAUTHORIZED_PWD" "Неправильный пароль" "STRYDER_RESPONSE" "Не удалось разобрать ответ Stryder" "PLAYER_NOT_FOUND" "Не удалось найти аккаунт игрока" - "INVALID_MASTERSERVER_TOKEN" "Некорректный или истёкший токен главного сервера. Перезапустите EA App, чтобы обновить токен." + "INVALID_MASTERSERVER_TOKEN" "Некорректный или истёкший токен главного сервера. Попробуйте перезапустить EA App." "JSON_PARSE_ERROR" "Ошибка разбора json-ответа" "UNSUPPORTED_VERSION" "Используемая вами версия больше не поддерживается" "DISABLE" "Выключить" @@ -280,7 +280,7 @@ "NO_RESULTS" "Нет результатов." "NO_MODS" "Нечего настраивать! Загрузите моды с ^5588FF00northstar.thunderstore.io^0." "respawnprotection" "Длит. защиты после возрождения" - "SNS_BANKRUPT_SUB" "Вас обнулил %s1" + "SNS_BANKRUPT_SUB" "Ваши очки сбросил %s1" "PL_hidden" "Невидимка" "PL_hidden_lobby" "Невидимка — лобби" "GAMEMODE_HIDDEN" "Невидимка" @@ -300,7 +300,7 @@ "PL_hidden_abbr" "НЕВИД" "SCOREBOARD_BANKRUPTS" "Врагов обанкрочено" "SNS_LEADER_BANKRUPT" "Лидер обанкрочен!" - "SNS_LEADER_BANKRUPT_SUB" "%s1 был обнулён игроком %s2" + "SNS_LEADER_BANKRUPT_SUB" "%s1 был сброшен игроком %s2" "gg_kill_reward" "Множитель награды за убийство" "gg_execution_reward" "Множитель награды за казнь" "PL_tffa" "Все против всех на титанах" @@ -322,8 +322,8 @@ "GAMEMODE_TFFA" "Все против всех на титанах" "SHOW_ONLY_DISABLED" "Только выключенные" "HIDE_LOCKED" "Скрыть недоступные" - "PL_chamber_desc" "Один выстрел — один труп. Заработайте ещё один патрон, убив врага первым." - "PL_chamber_hint" "Один выстрел — один труп. Заработайте ещё один патрон, убив врага первым." + "PL_chamber_desc" "Один выстрел — один труп. Один труп — ещё один выстрел." + "PL_chamber_hint" "Один выстрел — один труп. Один труп — ещё один выстрел." "PL_hidden_desc" "Один из игроков невидим." "PL_tffa_hint" "Каждый пилот сам за себя. Уничтожьте всех вражеских титанов." "sns_softball_kill_value" "Очков за убийство Софтболом" @@ -367,5 +367,16 @@ "MOD_FETCHING_FAILED_GENERAL" "Ошибка распаковки мода. Проверьте файл лога, чтобы узнать подробности." "MANIFESTO_FETCHING_TEXT" "Скачиваем список проверенных модов..." "MANIFESTO_FETCHING_TITLE" "Начало загрузки модов" + "MODE_MENU_FFA" "Все против всех" + "MODE_MENU_OTHER" "Другое" + "MODE_MENU_CUSTOM" "Свой" + "MODE_MENU_ALL" "Все" + "MODE_MENU_UNKNOWN" "Неизвестный" + "MODE_MENU_SWITCH" "Фильтр" + "MODE_MENU_TITAN_ONLY" "Только титаны" + "TEAMSWITCH_BUFFER" "Нельзя сменять команду так часто" + "TEAMSWITCH_GAMEMODE" "Невозможно сменить команду в текущем режиме игры" + "TEAMSWITCH_DISABLED" "Нельзя сменить команду в текущем режиме игры" + "TEAMSWITCH_GAMEPLAY" "Команду можно сменять только в игровой стадии" } } diff --git a/Northstar.Client/mod/resource/ui/menus/mode_select.menu b/Northstar.Client/mod/resource/ui/menus/mode_select.menu index f8afcf0ff..bf07164e7 100644 --- a/Northstar.Client/mod/resource/ui/menus/mode_select.menu +++ b/Northstar.Client/mod/resource/ui/menus/mode_select.menu @@ -1,343 +1,608 @@ -resource/ui/menus/mode_select.menu -{ - menu - { - ControlName Frame - xpos 0 - ypos 0 - zpos 3 - wide f0 - tall f0 - autoResize 0 - pinCorner 0 - visible 1 - enabled 1 - PaintBackgroundType 0 - infocus_bgcolor_override "0 0 0 0" - outoffocus_bgcolor_override "0 0 0 0" - - MenuCommon - { - ControlName CNestedPanel - xpos 0 - ypos 0 - wide f0 - tall f0 - visible 1 - controlSettingsFile "resource/ui/menus/panels/menu_common.res" - } - - MatchmakingStatus - { - ControlName CNestedPanel - xpos 0 - ypos 0 - wide f0 - tall f0 - visible 1 - controlSettingsFile "resource/ui/menus/panels/matchmaking_status.res" - } - - NextModeImageFrame - { - ControlName RuiPanel - xpos 800 - ypos 160 - wide 860 - tall 418 - labelText "" - visible 1 - bgcolor_override "0 0 0 0" - paintbackground 1 - rui "ui/basic_border_box.rpak" - } - - NextModeImage - { - ControlName RuiPanel - pin_to_sibling NextModeImageFrame - pin_corner_to_sibling BOTTOM - pin_to_sibling_corner BOTTOM -// xpos -12 - ypos -12 - wide 480 - tall 240 - visible 1 - scaleImage 1 - rui "ui/basic_menu_image.rpak" - zpos 2 - } - - ModeIconImage - { - ControlName RuiPanel - pin_to_sibling NextModeImage - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner TOP_LEFT - xpos 0 - ypos -16 - wide 72 - tall 72 - visible 1 - scaleImage 1 - rui "ui/basic_image_add.rpak" - zpos 2 - } - - NextModeName - { - ControlName Label - pin_to_sibling NextModeImageFrame - pin_corner_to_sibling TOP - pin_to_sibling_corner TOP - ypos -20 - wide 840 - auto_tall_tocontents 1 - visible 1 - labelText "Foo" - textAlignment center - centerWrap 1 - font Default_43_DropShadow - allcaps 1 - fgcolor_override "255 255 255 255" - } - - NextModeDesc - { - ControlName Label - pin_to_sibling NextModeName - pin_corner_to_sibling TOP - pin_to_sibling_corner BOTTOM - xpos 0 - ypos 0 - wide 840 - wrap 1 - auto_tall_tocontents 1 - visible 1 - labelText "Bar" - textAlignment center - centerWrap 1 - font Default_27 - allcaps 0 - fgcolor_override "255 255 255 255" - } - - MenuTitle - { - ControlName Label - InheritProperties MenuTitle - labelText "#SELECT_GAME_MODE" - } - - ButtonRowAnchor - { - ControlName Label - labelText "" - - xpos 96 - ypos 160 - } - - BtnMode1 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 0 - navUp BtnMode15 - navDown BtnMode2 - - pin_to_sibling ButtonRowAnchor - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner TOP_LEFT - } - BtnMode2 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 1 - pin_to_sibling BtnMode1 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode1 - navDown BtnMode3 - } - BtnMode3 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 2 - pin_to_sibling BtnMode2 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode2 - navDown BtnMode4 - } - BtnMode4 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 3 - pin_to_sibling BtnMode3 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - //ypos 11 - navUp BtnMode3 - navDown BtnMode5 - } - BtnMode5 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 4 - pin_to_sibling BtnMode4 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode4 - navDown BtnMode6 - } - BtnMode6 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 5 - pin_to_sibling BtnMode5 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode5 - navDown BtnMode7 - } - BtnMode7 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 6 - pin_to_sibling BtnMode6 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode6 - navDown BtnMode8 - } - BtnMode8 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 7 - pin_to_sibling BtnMode7 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode7 - navDown BtnMode9 - } - BtnMode9 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 8 - pin_to_sibling BtnMode8 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode8 - navDown BtnMode10 - } - BtnMode10 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 9 - pin_to_sibling BtnMode9 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode9 - navDown BtnMode11 - } - BtnMode11 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 10 - pin_to_sibling BtnMode10 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode10 - navDown BtnMode12 - } - BtnMode12 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 11 - pin_to_sibling BtnMode11 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode11 - navDown BtnMode13 - } - BtnMode13 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 12 - pin_to_sibling BtnMode12 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode12 - navDown BtnMode14 - } - BtnMode14 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 13 - pin_to_sibling BtnMode13 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode13 - navDown BtnMode15 - } - BtnMode15 - { - ControlName RuiButton - InheritProperties RuiSmallButton - classname ModeButton - scriptID 14 - pin_to_sibling BtnMode14 - pin_corner_to_sibling TOP_LEFT - pin_to_sibling_corner BOTTOM_LEFT - navUp BtnMode14 - navDown BtnMode1 - } - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - ButtonTooltip - { - ControlName CNestedPanel - InheritProperties ButtonTooltip - } - - FooterButtons - { - ControlName CNestedPanel - xpos 0 - ypos r119 - wide f0 - tall 36 - visible 1 - controlSettingsFile "resource/ui/menus/panels/footer_buttons.res" - } - } -} +resource/ui/menus/mode_select.menu +{ + menu + { + ControlName Frame + xpos 0 + ypos 0 + zpos 3 + wide f0 + tall f0 + autoResize 0 + pinCorner 0 + visible 1 + enabled 1 + PaintBackgroundType 0 + infocus_bgcolor_override "0 0 0 0" + outoffocus_bgcolor_override "0 0 0 0" + + MenuCommon + { + ControlName CNestedPanel + xpos 0 + ypos 0 + wide f0 + tall f0 + visible 1 + controlSettingsFile "resource/ui/menus/panels/menu_common.res" + } + + MatchmakingStatus + { + ControlName CNestedPanel + xpos 0 + ypos 0 + wide f0 + tall f0 + visible 1 + controlSettingsFile "resource/ui/menus/panels/matchmaking_status.res" + } + + MenuTitle + { + ControlName Label + InheritProperties MenuTitle + labelText "#SELECT_GAME_MODE" + } + + ButtonRowAnchor + { + ControlName Label + labelText "" + + xpos 96 + ypos 140 + } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// NEXT MODE PANEL +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + NextModeImageFrame + { + ControlName RuiPanel + xpos 740 + ypos 160 + wide 860 + tall 520 + labelText "" + visible 1 + bgcolor_override "0 0 0 0" + paintbackground 1 + rui "ui/control_options_description.rpak" + } + + NextModeImage + { + ControlName RuiPanel + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner TOP_LEFT + xpos 0 + ypos 14 + wide 480 + tall 240 + visible 1 + scaleImage 1 + rui "ui/basic_menu_image.rpak" + zpos 2 + } + + ModeIconImage + { + ControlName RuiPanel + pin_to_sibling NextModeImage + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner TOP_LEFT + xpos 0 + ypos -16 + wide 72 + tall 72 + visible 1 + scaleImage 1 + rui "ui/basic_image_add.rpak" + zpos 2 + } + + NextModeName + { + ControlName Label + pin_to_sibling NextModeImage + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + ypos -10 + xpos -10 + wide 840 + auto_tall_tocontents 1 + visible 1 + labelText "Foo" + //textAlignment center + //centerWrap 1 + font Default_43_DropShadow + allcaps 1 + fgcolor_override "255 255 255 255" + } + + NextModeDesc + { + ControlName Label + pin_to_sibling NextModeName + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + xpos 0 + ypos 10 + wide 840 + wrap 1 + auto_tall_tocontents 1 + visible 1 + labelText "Bar" + //textAlignment center + //centerWrap 1 + font Default_27 + allcaps 0 + fgcolor_override "255 255 255 255" + } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// FILTERS PANEL +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + FiltersPanel + { + ControlName RuiPanel + xpos 740 + ypos 682 + wide 860 + tall 156 + zpos -1 + + rui "ui/control_options_description.rpak" + } + + BtnModeLabel + { + ControlName RuiButton + InheritProperties RuiSmallButton + labelText "#SEARCHBAR_LABEL" + textAlignment west + classname FilterPanelChild + + wide 500 + xpos -18 + ypos -16 + + pin_to_sibling FiltersPanel + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner TOP_LEFT + } + + BtnModeSearch + { + 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 0 + unicode 1 + + pin_to_sibling BtnModeLabel + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner TOP_RIGHT + } + + SwtModeLabel + { + ControlName RuiButton + InheritProperties SwitchButton + labelText "#MODE_MENU_FILTER" + ConVar "modemenu_mode_filter" + classname FilterPanelChild + wide 500 + ypos 2 + + pin_to_sibling BtnModeLabel + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + BtnModeFiltersClear + { + ControlName RuiButton + InheritProperties RuiSmallButton + labelText "#CLEAR_FILTERS" + textAlignment west + classname FilterPanelChild + + wide 100 + ypos 2 + + pin_to_sibling SwtModeLabel + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// PANELS LIST +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + Panel1 + { + ControlName CNestedPanel + classname ModeSelectorPanel + scriptID 1 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling ButtonRowAnchor + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel2 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 2 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel1 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel3 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 3 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel2 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel4 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 4 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel3 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel5 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 5 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel4 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel6 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 6 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel5 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel7 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 7 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel6 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel8 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 8 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel7 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel9 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 9 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel8 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel10 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 10 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel9 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel11 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 11 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel10 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel12 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 12 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel11 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel13 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 13 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel12 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel14 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 14 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel13 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + + Panel15 + { + ControlName "CNestedPanel" + classname ModeSelectorPanel + scriptID 15 + + controlSettingsFile "resource/ui/menus/panels/mode_select_button.res" + wide %100 + tall 45 + + pin_to_sibling Panel14 + pin_corner_to_sibling TOP_LEFT + pin_to_sibling_corner BOTTOM_LEFT + } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SLIDER +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + BtnModeListUpArrow + { + ControlName RuiButton + InheritProperties RuiSmallButton + //labelText "A" + wide 40 + tall 40 + xpos 2 + ypos 2 + + image "vgui/hud/white" + drawColor "255 255 255 128" + + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_RIGHT + pin_to_sibling_corner TOP_LEFT + } + + BtnModeListUpArrowPanel + { + ControlName RuiPanel + wide 40 + tall 40 + xpos 2 + ypos 2 + + rui "ui/control_options_description.rpak" + + visible 1 + zpos -1 + + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_RIGHT + pin_to_sibling_corner TOP_LEFT + } + + BtnModeListDownArrow + { + ControlName RuiButton + InheritProperties RuiSmallButton + //labelText "V" + wide 40 + tall 40 + xpos 2 + ypos -639 + + image "vgui/hud/white" + drawColor "255 255 255 128" + + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_RIGHT + pin_to_sibling_corner TOP_LEFT + } + + BtnModeListDownArrowPanel + { + ControlName RuiPanel + wide 40 + tall 40 + xpos 2 + ypos -639 + + rui "ui/control_options_description.rpak" + + visible 1 + zpos -1 + + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_RIGHT + pin_to_sibling_corner TOP_LEFT + } + + BtnModeListSlider + { + ControlName RuiButton + InheritProperties RuiSmallButton + //labelText "V" + wide 40 + tall 599 + xpos 2 + ypos -42 + zpos 0 + + image "vgui/hud/white" + drawColor "255 255 255 128" + + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_RIGHT + pin_to_sibling_corner TOP_LEFT + } + + BtnModeListSliderPanel + { + ControlName RuiPanel + wide 40 + tall 599 + xpos 2 + ypos -42 + + rui "ui/control_options_description.rpak" + + visible 1 + zpos -1 + + pin_to_sibling NextModeImageFrame + 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 562 + xpos 2 + ypos -42 + zpos 1 + + pin_to_sibling NextModeImageFrame + pin_corner_to_sibling TOP_RIGHT + pin_to_sibling_corner TOP_LEFT + } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + ButtonTooltip + { + ControlName CNestedPanel + InheritProperties ButtonTooltip + } + + FooterButtons + { + ControlName CNestedPanel + xpos 0 + ypos r119 + wide f0 + tall 36 + visible 1 + controlSettingsFile "resource/ui/menus/panels/footer_buttons.res" + } + } +} diff --git a/Northstar.Client/mod/resource/ui/menus/panels/mode_select_button.res b/Northstar.Client/mod/resource/ui/menus/panels/mode_select_button.res new file mode 100644 index 000000000..b361e4faa --- /dev/null +++ b/Northstar.Client/mod/resource/ui/menus/panels/mode_select_button.res @@ -0,0 +1,31 @@ +resource/ui/menus/panels/mode_select_button.res +{ + BtnMode + { + ControlName RuiButton + InheritProperties RuiSmallButton + classname ModButton + labelText "please show up" + wide 600 + tall 45 + + pin_to_sibling ControlBox + pin_corner_to_sibling LEFT + pin_to_sibling_corner RIGHT + } + + Header + { + ControlName Label + InheritProperties RuiSmallButton + wide 600 + labelText "labelText" + font Default_41 + fgcolor_override "255 255 255 255" + tall 45 + + pin_to_sibling ControlBox + pin_corner_to_sibling LEFT + pin_to_sibling_corner RIGHT + } +} diff --git a/Northstar.Client/mod/resource/ui/menus/server_browser.menu b/Northstar.Client/mod/resource/ui/menus/server_browser.menu index 4a84a714a..03cdd0e8c 100644 --- a/Northstar.Client/mod/resource/ui/menus/server_browser.menu +++ b/Northstar.Client/mod/resource/ui/menus/server_browser.menu @@ -2554,6 +2554,7 @@ resource/ui/menus/mods_browse.menu xpos -17 ypos -57 zpos 90 + textAlignment north scriptID 999 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 9e683a869..e4e44d51e 100644 --- a/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut +++ b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut @@ -29,6 +29,19 @@ global struct UIPresenceStruct { int gameState } +global struct ModInfo +{ + string name = "" + string description = "" + string version = "" + string downloadLink = "" + int loadPriority = 0 + bool enabled = false + bool requiredOnClient = false + bool isRemote + array conVars = [] +} + global struct RequiredModInfo { string name diff --git a/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut b/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut new file mode 100644 index 000000000..421bd097d --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/client/cl_damage_indicator.gnut @@ -0,0 +1,951 @@ +untyped + +global function ClDamageIndicator_Init +global function Create_DamageIndicatorHUD +global function DamageIndicators +global function GrenadeArrowThink +global function RumbleForTitanDamage + +global function ServerCallback_TitanTookDamage +global function ServerCallback_PilotTookDamage +//global function ClientCodeCallback_OnMissileCreation +global function ClientCodeCallback_CreateGrenadeIndicator + +global function DamageIndicatorRui + +global function ShowGrenadeArrow + +global function SCB_AddGrenadeIndicatorForEntity + +const DAMAGEARROW_FADEANIM = "damage_fade" +const DAMAGEARROW_DURATION = 2.5 +const DAMAGEARROW_SMALL = 0 +const DAMAGEARROW_MEDIUM = 1 +const DAMAGEARROW_LARGE = 2 + +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME = 0.4 +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED = 0.15 +const float DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED_VELOCITYCUTOFF = 500.0 + + +struct { + array damageArrows + int currentDamageArrow = 0 + int numDamageArrows = 16 + float damageArrowFadeDuration = 1.0 + float damageArrowTime = 0.0 + vector damageArrowAngles = < 0.0, 0.0, 0.0 > + vector damageArrowPointCenter = < 0.0, 0.0, 0.0 > + + table whizByFX = { + small = null, + large = null, + titan = null, + } + + array
arrowIncomingAnims = [ + { anim = "damage_incoming_small", duration = 1.5 }, + { anim = "damage_incoming", duration = 1.75 }, + { anim = "damage_incoming_large", duration = 2.00 }, + ] + + int damageIndicatorCount = 0 +} file + +function ClDamageIndicator_Init() +{ + RegisterSignal( "CriticalHitReceived" ) + + AddCreateCallback( "titan_cockpit", DamageArrow_CockpitInit ) + + PrecacheParticleSystem( $"P_wpn_grenade_frag_icon" ) + PrecacheParticleSystem( $"P_wpn_grenade_frag_blue_icon" ) + PrecacheParticleSystem( $"P_wpn_grenade_smoke_icon" ) + + if ( !IsLobby() ) + AddCallback_EntitiesDidLoad( InitDamageArrows ) +} + +function ServerCallback_TitanTookDamage( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId, doomedNow, doomedDamage ) +{ + expect float( damage ) + expect int( damageType ) + expect int( damageSourceId ) + expect bool( doomedNow ) + expect int( doomedDamage ) + + if ( IsWatchingThirdPersonKillReplay() ) + return + + if ( DebugVictimClientDamageFeedbackIsEnabled() && (damage > 0.0) ) + { + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + bool isCritical = (damageType & DF_CRITICAL) ? true : false + bool isDoomProtected = (damageType & DF_DOOM_PROTECTED) ? true : false + bool isDoomFatality = (damageType & DF_DOOM_FATALITY) ? true : false + + local weaponMods = [] + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( modNameStrings[eModId] ) + + string modDesc = ((eModId != null && eModId in modNameStrings) ? (expect string( modNameStrings[eModId] )) : "") + DebugTookDamagePrint( localViewPlayer, attacker, damage, damageSourceId, modDesc, isHeadShot, isKillShot, isCritical, doomedNow, doomedDamage, isDoomProtected, isDoomFatality ) + } + + // It appears to be faster here to create a new thread so other functions called can wait until the frame ends before running. + thread TitanTookDamageThread( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId, doomedNow, doomedDamage ) + + vector damageOrigin = < x, y, z > + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + + if ( damageSourceId in clGlobal.onLocalPlayerTookDamageCallback ) + { + foreach ( callback in clGlobal.onLocalPlayerTookDamageCallback[ damageSourceId ] ) + callback( damage, damageOrigin, damageType, damageSourceId, attacker ) + } +} + +function TitanTookDamageThread( float damage, x, y, z, int damageType, int damageSourceId, attackerEHandle, eModId, bool doomedNow, int doomedDamage ) +{ + WaitEndFrame() + + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + entity cockpit = localViewPlayer.GetCockpit() + + if ( cockpit && IsTitanCockpitModelName( cockpit.GetModelName() ) ) + TitanCockpit_DamageFeedback( localViewPlayer, cockpit, damage, damageType, < x, y, z >, damageSourceId, doomedNow, doomedDamage ) + + if ( damage >= DAMAGE_BREAK_MELEE_ASSIST ) + localViewPlayer.Lunge_ClearTarget() + + if ( damageSourceId != eDamageSourceId.bubble_shield ) //Don't play Betty OS dialogue if we took damage by bubble shield. We don't have appropriate dialogue for it. + Tracker_PlayerAttackedByTarget( localViewPlayer, attacker ) + + array weaponMods + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( expect string( modNameStrings[ eModId ] ) ) + + if ( (damage > 0.0) || doomedDamage ) + { + vector damageOrigin = < x, y, z > + DamageHistoryStruct damageHistory = StoreDamageHistoryAndUpdate( localViewPlayer, MAX_DAMAGE_HISTORY_TIME, damage, damageOrigin, damageType, damageSourceId, attacker, weaponMods ) + DamageIndicators( damageHistory, true ) + } + + entity soul = localViewPlayer.GetTitanSoul() + if ( PlayerHasPassive( localViewPlayer, ePassives.PAS_AUTO_EJECT ) ) //TODO: Handle nuclear eject if we ever allow nuclear + auto eject combo again + { + if ( ShouldPlayAutoEjectAnim( localViewPlayer, soul, doomedNow ) ) + thread PlayerEjects( localViewPlayer, cockpit ) + + } + + if ( damageType & DF_CRITICAL ) + { + localViewPlayer.Signal( "CriticalHitReceived" ) + EmitSoundOnEntity( localViewPlayer, "titan_damage_crit_3p_vs_1p" ) + } +} + +bool function ShouldPlayAutoEjectAnim( entity player, entity titanSoul, bool doomedNow ) +{ + if ( !titanSoul.IsDoomed() ) + return false + + if ( player.ContextAction_IsActive() && !player.ContextAction_IsBusy() ) //Some other context action, e.g. melee instead of eject. Then again + return false + + return true +} + +string function DevBuildAttackerDesc( entity localViewPlayer, entity ent ) +{ + if ( ent == null ) + return "" + + if ( localViewPlayer == ent ) + return ("") + + if ( ent.IsPlayer() ) + return ("'" + ent.GetPlayerName() + "' " + ent.GetPlayerSettings()) + + entity bossPlayer = ent.GetBossPlayer() + string ownerString = ((bossPlayer != null) ? (bossPlayer.GetPlayerName() + "'s ") : "") + + var sigName = ent.GetSignifierName() + string debugName = (sigName != null) ? expect string( sigName ) : ent.GetClassName() + return (ownerString + debugName) +} + +void function DebugTookDamagePrint( entity ornull localViewPlayer, entity attacker, float damage, int damageSourceId, string modDesc, bool isHeadShot, bool isKillShot, bool isCritical, bool isDoomShot, int doomShotDamage, bool isDoomProtected, bool isDoomFatality ) +{ + Assert( localViewPlayer ) + string attackerDesc = DevBuildAttackerDesc( expect entity( localViewPlayer ), attacker ) + string timePrint = format( "%d:%.2f", FrameCount(), PlatformTime() ) + printt( + "{"+timePrint+"} TOOK DAMAGE: " + damage + + (isHeadShot ? " (headshot)" : "") + + (isCritical ? " (critical)" : "") + + (isKillShot ? " (KILLED)" : "") + + (isDoomShot ? " (DOOMED dmg:" + doomShotDamage + ")" : "") + + (isDoomProtected ? " (DOOM PROTECTION)" : "") + + (isDoomFatality ? " (DOOM FATALITY)" : "") + + " " + attackerDesc + + " w/ " + GetObitFromDamageSourceID( damageSourceId ) + modDesc + ) +} + +void function PlayVictimHeadshotConfirmation( bool isKillShot ) +{ + entity localViewPlayer = GetLocalViewPlayer() + if ( localViewPlayer == null ) + return + + if ( isKillShot ) + EmitSoundOnEntity( localViewPlayer, "Player.Hitbeep_headshot.Kill.Human_3P_vs_1P" ) + else + EmitSoundOnEntity( localViewPlayer, "Player.Hitbeep_headshot.Human_3P_vs_1P" ) +} + +void function RumbleForPilotDamage( float damageAmount ) +{ + Rumble_Play( "rumble_pilot_hurt", {} ) +} + +void function RumbleForTitanDamage( float damageAmount ) +{ + string rumbleName; + if ( damageAmount < 500 ) + rumbleName = "titan_damaged_small" + else if ( damageAmount < 1000 ) + rumbleName = "titan_damaged_med" + else + rumbleName = "titan_damaged_big" + + Rumble_Play( rumbleName, {} ) +} + +function ServerCallback_PilotTookDamage( damage, x, y, z, damageType, damageSourceId, attackerEHandle, eModId ) +{ + expect float( damage ) + expect int( damageType ) + expect int( damageSourceId ) + + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localViewPlayer = GetLocalViewPlayer() + vector damageOrigin = < x, y, z > + + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + + if ( isHeadShot ) + PlayVictimHeadshotConfirmation( isKillShot ); + + //Jolt view if player is getting meleed + if ( damageSourceId == eDamageSourceId.human_melee ) + { + vector joltDir = Normalize( localViewPlayer.CameraPosition() - damageOrigin ) + //clear melee assist when you get meleed + localViewPlayer.Lunge_ClearTarget() + } + + array weaponMods + if ( eModId != null && eModId in modNameStrings ) + weaponMods.append( expect string( modNameStrings[ eModId ] ) ) + + if ( DebugVictimClientDamageFeedbackIsEnabled() && !IsWatchingReplay() ) + { + string modDesc = (weaponMods.len() > 0 ? (" +" + weaponMods[0]) : "") + bool isCritical = (damageType & DF_CRITICAL) ? true : false + + DebugTookDamagePrint( localViewPlayer, attacker, damage, damageSourceId, modDesc, isHeadShot, isKillShot, isCritical, false, 0, false, false ) + } + + RumbleForPilotDamage( damage ) + + DamageHistoryStruct damageTable = StoreDamageHistoryAndUpdate( localViewPlayer, MAX_DAMAGE_HISTORY_TIME, damage, damageOrigin, damageType, damageSourceId, attacker, weaponMods ) + + DamageIndicators( damageTable, false ) + + if ( damageSourceId in clGlobal.onLocalPlayerTookDamageCallback ) + { + foreach ( callback in clGlobal.onLocalPlayerTookDamageCallback[ damageSourceId ] ) + callback( damage, damageOrigin, damageType, damageSourceId, attacker ) + } +} + +/* +void function ClientCodeCallback_OnMissileCreation( entity missileEnt, string weaponName, bool firstTime ) +{ + +} +*/ + +void function ClientCodeCallback_CreateGrenadeIndicator( entity missileEnt, string weaponName ) +{ + if ( !IsValid( missileEnt ) ) + return + + //Called for all projectiles, not just missiles. + TryAddGrenadeIndicator( missileEnt, weaponName ) +} + + +void function DamageIndicators( DamageHistoryStruct damageHistory, bool playerIsTitan ) +{ + if ( damageHistory.damageType & DF_NO_INDICATOR ) + return + if ( !level.clientScriptInitialized ) + return + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity localViewPlayer = GetLocalViewPlayer() + + int arrowType = DAMAGEARROW_MEDIUM + + if ( IsValid( damageHistory.attacker ) ) + { + if ( damageHistory.attacker == localViewPlayer ) + return + + if ( damageHistory.attacker.IsTitan() ) + arrowType = DAMAGEARROW_MEDIUM + else if ( damageHistory.attacker.IsPlayer() ) + arrowType = DAMAGEARROW_SMALL + else + arrowType = DAMAGEARROW_SMALL + + //if ( damageHistory.attacker.IsTitan() ) + // arrowType = DAMAGEARROW_LARGE + //else if ( damageHistory.attacker.IsPlayer() ) + // arrowType = DAMAGEARROW_MEDIUM + //else + // arrowType = DAMAGEARROW_SMALL + } + + if ( playerIsTitan ) + { + entity cockpit = localViewPlayer.GetCockpit() + + if ( !cockpit ) + return + + vector dirToDamage = damageHistory.origin - localViewPlayer.GetOrigin() + dirToDamage.z = 0 + dirToDamage = Normalize( dirToDamage ) + + vector playerViewForward = localViewPlayer.GetViewVector() + playerViewForward.z = 0.0 + playerViewForward = Normalize( playerViewForward ) + + float damageFrontDot = DotProduct( dirToDamage, playerViewForward ) + + if ( damageFrontDot >= 0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_TOP, damageHistory.damage ) + else if ( damageFrontDot <= -0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_BOTTOM, damageHistory.damage ) + else + { + vector playerViewRight = localViewPlayer.GetViewRight() + playerViewRight.z = 0.0 + playerViewRight = Normalize( playerViewRight ) + + float damageRightDot = DotProduct( dirToDamage, playerViewRight ) + + if ( damageRightDot >= 0.707107 ) + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_RIGHT, damageHistory.damage ) + else + cockpit.AddToTitanHudDamageHistory( COCKPIT_PANEL_LEFT, damageHistory.damage ) + } + + if ( damageHistory.damageSourceId != eDamageSourceId.mp_weapon_arc_launcher && damageHistory.attacker && damageHistory.attacker.GetParent() == localViewPlayer ) + { + damageHistory.rodeoDamage = true + return + } + } + + #if SP + if ( IsValid( damageHistory.attacker ) && damageHistory.attacker.IsTitan() ) + arrowType = DAMAGEARROW_LARGE + else if ( playerIsTitan && damageHistory.damage < 50 ) + return + else if ( !playerIsTitan && damageHistory.damage < 15 ) + arrowType = DAMAGEARROW_SMALL + else + arrowType = DAMAGEARROW_MEDIUM + + thread DamageIndicatorRui( damageHistory.origin, arrowType, playerIsTitan ) + #else + bool show2DIndicator = true + bool show3DIndicator = false + + const int DAMAGE_INDICATOR_STYLE_2D_ONLY = 0 + const int DAMAGE_INDICATOR_STYLE_BOTH = 1 + const int DAMAGE_INDICATOR_STYLE_3D_ONLY = 2 + + if ( playerIsTitan ) + { + show2DIndicator = GetConVarInt( "damage_indicator_style_titan" ) != DAMAGE_INDICATOR_STYLE_3D_ONLY + show3DIndicator = GetConVarInt( "damage_indicator_style_titan" ) != DAMAGE_INDICATOR_STYLE_2D_ONLY + } + else + { + show2DIndicator = GetConVarInt( "damage_indicator_style_pilot" ) != DAMAGE_INDICATOR_STYLE_3D_ONLY + show3DIndicator = GetConVarInt( "damage_indicator_style_pilot" ) != DAMAGE_INDICATOR_STYLE_2D_ONLY + } + + if ( show2DIndicator ) + thread DamageIndicatorRui( damageHistory.origin, arrowType, playerIsTitan ) + + if ( show3DIndicator ) + ShowDamageArrow( localViewPlayer, damageHistory.origin, arrowType, playerIsTitan, damageHistory.attacker ) + #endif +} + +const float DAMAGE_INDICATOR_DURATION = 4.0 + +void function DamageIndicatorRui( vector damageOrigin, int arrowType, bool playerIsTitan ) +{ + clGlobal.levelEnt.EndSignal( "KillReplayStarted" ) + clGlobal.levelEnt.EndSignal( "KillReplayEnded" ) + + // slop + float distance = Length( damageOrigin - GetLocalViewPlayer().CameraPosition() ) + float randomRange = GraphCapped( distance, 0.0, 2048, 0.0, 256.0 ) + damageOrigin = + + float startTime = Time() + + var rui = RuiCreate( $"ui/damage_indicator.rpak", clGlobal.topoFullScreen, RUI_DRAW_HUD, 0 ) + RuiSetResolutionToScreenSize( rui ) + RuiSetGameTime( rui, "startTime", startTime ) + RuiSetFloat( rui, "duration", DAMAGE_INDICATOR_DURATION ) + RuiSetInt( rui, "attackerType", arrowType ) + + file.damageIndicatorCount++ + int damageIndicatorThreshold = file.damageIndicatorCount + 8 + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + while ( Time() - startTime < DAMAGE_INDICATOR_DURATION && file.damageIndicatorCount < damageIndicatorThreshold ) + { + vector vecToDamage = damageOrigin - GetLocalViewPlayer().CameraPosition() + vecToDamage.z = 0 + vecToDamage = Normalize( vecToDamage ) + RuiSetFloat3( rui, "vecToDamage2D", vecToDamage ) + RuiSetFloat3( rui, "camVec2D", Normalize( AnglesToForward( < 0, GetLocalViewPlayer().CameraAngles().y, 0 > ) ) ) + RuiSetFloat( rui, "sideDot", vecToDamage.Dot( CrossProduct( <0, 0, 1>, Normalize( AnglesToForward( < 0, GetLocalViewPlayer().CameraAngles().y, 0 > ) ) ) ) ) + WaitFrame() + } +} + +void function ShowGrenadeArrow( entity player, entity grenade, float damageRadius, float startDelay, bool requireLos = true ) +{ + thread GrenadeArrowThink( player, grenade, damageRadius, startDelay, requireLos ) +} + +vector function GetRandomOriginWithinBounds( entity ent ) +{ + vector boundingMins = ent.GetBoundingMins() + vector boundingMaxs = ent.GetBoundingMaxs() + + vector randomOffset = < RandomFloatRange( boundingMins.x, boundingMaxs.x ), RandomFloatRange( boundingMins.y, boundingMaxs.y ), RandomFloatRange( boundingMins.z, boundingMaxs.z ) > + + return ent.GetOrigin() + randomOffset +} + +void function GrenadeArrowThink( entity player, entity grenade, float damageRadius, float startDelay, bool requireLos, string requiredPlayerState = "any" ) +{ + EndSignal( grenade, "OnDeath" ) //On death added primarily for frag_drones + EndSignal( grenade, "OnDestroy" ) + EndSignal( player, "OnDeath" ) + + wait startDelay + + asset grenadeModel = GRENADE_INDICATOR_FRAG_MODEL + vector grenadeOffset = < -5, 0, 0 > + if ( grenade instanceof C_Projectile ) + { + if ( grenade.ProjectileGetWeaponClassName() == "mp_weapon_grenade_sonar" ) + { + grenadeModel = GRENADE_INDICATOR_SONAR_MODEL + grenadeOffset = < -5, 0, 0 > + requireLos = false + } + } + else if ( grenade.IsNPC() ) + { + switch ( grenade.GetSignifierName() ) + { + #if MP + case "npc_stalker": + grenadeModel = GRENADE_INDICATOR_STALKER_MODEL + break + #endif + + case "npc_frag_drone": + grenadeModel = GRENADE_INDICATOR_TICK_MODEL + break + } + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, GRENADE_INDICATOR_ARROW_MODEL ) + entity mdl = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, grenadeModel ) + + OnThreadEnd( + function() : ( arrow, mdl ) + { + if ( IsValid( arrow ) ) + arrow.Destroy() + if ( IsValid( mdl ) ) + mdl.Destroy() + } + ) + + entity cockpit = player.GetCockpit() + if ( !cockpit ) + return + + EndSignal( cockpit, "OnDestroy" ) + + arrow.SetParent( cockpit, "CAMERA_BASE" ) + arrow.SetAttachOffsetOrigin( < 25.0, 0.0, -4.0 > ) + + mdl.SetParent( arrow, "BACK" ) + mdl.SetAttachOffsetOrigin( grenadeOffset ) + + float lastVisibleTime = 0 + bool shouldBeVisible = true + + while ( true ) + { + cockpit = player.GetCockpit() + + switch ( requiredPlayerState ) + { + case "any": + shouldBeVisible = true + break + case "pilot": + shouldBeVisible = !player.IsTitan() + break + case "titan": + shouldBeVisible = player.IsTitan() + break + default: + Assert( false, "Invalid player state! Allower states: 'any' 'pilot' 'titan'" ) + + } + + if ( shouldBeVisible ) + { + if ( Distance( player.GetOrigin(), grenade.GetOrigin() ) > damageRadius || !cockpit ) + { + shouldBeVisible = false + } + else + { + bool tracePassed = false + + if ( requireLos ) + { + TraceResults result = TraceLine( grenade.GetOrigin(), GetRandomOriginWithinBounds( player ), grenade, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + if ( result.fraction == 1.0 ) + tracePassed = true + } + + if ( requireLos && !tracePassed ) + { + shouldBeVisible = false + } + else + { + shouldBeVisible = true + lastVisibleTime = Time() + } + } + } + + if ( shouldBeVisible || Time() - lastVisibleTime < 0.25 ) + { + arrow.EnableDraw() + mdl.EnableDraw() + + arrow.DisableRenderWithViewModelsNoZoom() + arrow.EnableRenderWithCockpit() + arrow.EnableRenderWithHud() + mdl.DisableRenderWithViewModelsNoZoom() + mdl.EnableRenderWithCockpit() + mdl.EnableRenderWithHud() + + vector damageArrowAngles = AnglesInverse( player.EyeAngles() ) + vector vecToDamage = grenade.GetOrigin() - (player.EyePosition() + (player.GetViewVector() * 20.0)) + + // reparent for embark/disembark + if ( arrow.GetParent() == null ) + arrow.SetParent( cockpit, "CAMERA_BASE", true ) + + arrow.SetAttachOffsetAngles( damageArrowAngles.AnglesCompose( vecToDamage.VectorToAngles() ) ) + } + else + { + mdl.DisableDraw() + arrow.DisableDraw() + } + + WaitFrame() + } + +} + + +function Create_DamageIndicatorHUD() +{ +} + + +void function SCB_AddGrenadeIndicatorForEntity( int team, int ownerHandle, int eHandle, float damageRadius ) +{ + if ( !level.grenadeIndicatorEnabled ) + return + + #if DEV + if ( !level.clientScriptInitialized ) + return + #endif + + entity player = GetLocalViewPlayer() + entity owner = GetEntityFromEncodedEHandle( ownerHandle ) + + entity ent = GetEntityFromEncodedEHandle( eHandle ) + if ( !IsValid( ent ) ) + return + + if ( team == player.GetTeam() && owner != player ) + return + + //TryAddGrenadeIndicator( ent, "" ) // TODO: make function handle non-grenade ents +} + + +function TryAddGrenadeIndicator( grenade, weaponName ) +{ + #if DEV + if ( !level.clientScriptInitialized ) + return + #endif + + if ( !level.grenadeIndicatorEnabled ) + return + + expect entity( grenade ) + entity player = GetLocalViewPlayer() + + // view player may be null when dead + if ( !IsValid( player ) ) + return + + var className = grenade.GetClassName() + float damageRadius = 0.0 + + if ( className == "grenade" ) + { + damageRadius = grenade.GetDamageRadius() + } + else if ( grenade.ProjectileGetWeaponClassName() == "mp_titanweapon_arc_ball" ) + { + // arc ball doesn't arc to pilots so no need to show the warning + if ( !player.IsTitan() ) + return + + damageRadius = BALL_LIGHTNING_ZAP_RADIUS + } + else + { + return + } + + float radius = grenade.GetExplosionRadius() + + if ( player.IsPhaseShifted() ) + return + + + float startDelay = 0.0 + if ( grenade.GetOwner() == player ) + { + if ( !grenade.GetProjectileWeaponSettingBool( eWeaponVar.projectile_damages_owner ) && !grenade.GetProjectileWeaponSettingBool( eWeaponVar.explosion_damages_owner ) ) + return + + float relVelocity = Length( grenade.GetVelocity() - player.GetVelocity() ) + if ( relVelocity < DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED_VELOCITYCUTOFF ) + startDelay = DAMAGEHUD_GRENADE_DEBOUNCE_TIME_LOWSPEED + else + startDelay = DAMAGEHUD_GRENADE_DEBOUNCE_TIME + } + else if ( grenade.GetTeam() == player.GetTeam() ) + { + return + } + + float padding = player.IsTitan() ? 204.0 : 65.0 + + //AddGrenadeIndicator( grenade, radius + padding, startDelay, false ) + ShowGrenadeArrow( player, grenade, radius + padding, startDelay ) + + //thread ShowRuiGrenadeThreatIndicator( grenade, float( radius ) + padding ) +} + +void function ShowRuiGrenadeThreatIndicator( entity grenade, float radius ) +{ + var rui = RuiCreate( $"ui/grenade_threat_indicator.rpak", clGlobal.topoCockpitHudPermanent, RUI_DRAW_COCKPIT, 0 ) + //var rui = CreateCockpitRui( $"ui/grenade_threat_indicator.rpak", 0 ) + RuiSetGameTime( rui, "startTime", Time() ) + RuiSetFloat( rui, "damageRadius", radius ) + //RuiTrackFloat3( rui, "pos", grenade, RUI_TRACK_ABSORIGIN_FOLLOW )` + RuiTrackFloat3( rui, "pos", grenade, RUI_TRACK_POINT_FOLLOW, grenade.LookupAttachment( "BACK" ) ) + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + grenade.WaitSignal( "OnDestroy" ) +} + + + +void function InitDamageArrows() +{ + for ( int i = 0; i < file.numDamageArrows; i++ ) + { + table arrowData = { + grenade = null + grenadeRadius = 0.0 + damageOrigin = < 0.0, 0.0, 0.0 >, + damageDirection = < 0.0, 0.0, 0.0 >, + endTime = -99.0 + DAMAGEARROW_DURATION, + startTime = -99.0, + isDying = false, + isVisible = false, + whizby = false, // hack until we get a new model/shader for the whizby indicator - Roger + attacker = null, + randomAngle = 0 // Repeated shots from the same attacker randomize the angle of the arrow. + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, DAMAGEARROW_MODEL ) + arrow.SetCanCloak( false ) + arrow.SetVisibleForLocalPlayer( 0 ) + arrow.DisableDraw() + + arrowData.arrow <- arrow + arrow.s.arrowData <- arrowData + + file.damageArrows.append( arrowData ) + } + + entity arrow = CreateClientSidePropDynamic( < 0, 0, 0 >, < 0, 0, 0 >, DAMAGEARROW_MODEL ) + file.damageArrowFadeDuration = arrow.GetSequenceDuration( DAMAGEARROW_FADEANIM ) // 0.266 + arrow.Destroy() +} + + +void function DamageArrow_CockpitInit( entity cockpit ) +{ + entity localViewPlayer = GetLocalViewPlayer() + thread UpdateDamageArrows( localViewPlayer, cockpit ) +} + +function RefreshExistingDamageArrow( entity player, arrowData, int arrowType, damageOrigin ) +{ + //Hack - 10 tick rate is making damage feedback bunch up. If we improve that then shouldn't be threaded. + player.EndSignal( "OnDestroy" ) + entity cockpit = player.GetCockpit() + if ( IsValid( cockpit ) ) + cockpit.EndSignal( "OnDestroy" ) + + float time = Time() + + if ( arrowData.startTime == time ) + wait 0.05 + + if ( !arrowData.isVisible || arrowData.isDying ) + return + + time = Time() + arrowData.endTime = time + file.arrowIncomingAnims[ arrowType ].duration + arrowData.startTime = time + arrowData.damageOrigin = damageOrigin + arrowData.randomAngle = RandomIntRange( -3, 3 ) + PulseDamageArrow( expect entity( arrowData.arrow ), arrowType ) + UpdateDamageArrowVars( player ) + UpdateDamageArrowAngle( arrowData ) +} + +function ShowDamageArrow( entity player, damageOrigin, int arrowType, playerIsTitan, attacker ) +{ + if ( file.damageArrows.len() == 0 ) // not yet initialized + return + + table arrowData = file.damageArrows[file.currentDamageArrow] + entity arrow = expect entity( arrowData.arrow ) + + file.currentDamageArrow++ + if ( file.currentDamageArrow >= file.numDamageArrows ) + file.currentDamageArrow = 0 + + float time = Time() + + arrow.s.arrowData.damageOrigin = damageOrigin + arrow.s.arrowData.grenade = null + arrow.s.arrowData.grenadeRadius = 0.0 + arrow.s.arrowData.endTime = time + file.arrowIncomingAnims[ arrowType ].duration + arrow.s.arrowData.startTime = time + arrow.s.arrowData.isDying = false + arrow.s.arrowData.whizby = false // hack until we get a new model/shader for the whizby indicator + arrow.s.arrowData.attacker = attacker + + if ( !arrow.s.arrowData.isVisible ) + { + entity cockpit = player.GetCockpit() + + if ( !cockpit ) + return + + arrow.s.arrowData.isVisible = true + arrow.EnableDraw() + + arrow.DisableRenderWithViewModelsNoZoom() + arrow.EnableRenderWithCockpit() + + arrow.EnableRenderWithHud() + + arrow.SetParent( cockpit, "CAMERA_BASE" ) + arrow.SetAttachOffsetOrigin( < 20.0, 0.0, -2.0 > ) + } + + + PulseDamageArrow( arrow, arrowType ) + UpdateDamageArrowVars( player ) + UpdateDamageArrowAngle( arrowData ) +} + + +function PulseDamageArrow( entity arrow, int arrowType ) +{ + arrow.Anim_NonScriptedPlay( file.arrowIncomingAnims[ arrowType ].anim ) +} + +function UpdateDamageArrowVars( entity localViewPlayer ) +{ + file.damageArrowTime = Time() + file.damageArrowAngles = AnglesInverse( localViewPlayer.EyeAngles() ) + file.damageArrowPointCenter = localViewPlayer.EyePosition() + ( localViewPlayer.GetViewVector() * 20.0 ) +} + +function UpdateDamageArrowAngle( arrowData ) +{ + if ( IsValid( arrowData.grenade ) ) + arrowData.damageOrigin = arrowData.grenade.GetOrigin() + + vector vecToDamage = expect vector( arrowData.damageOrigin ) - file.damageArrowPointCenter + vector anglesToDamage = VectorToAngles( vecToDamage ) + vector eyeAngles = GetLocalViewPlayer().EyeAngles() + + float roll = sin( DegToRad( eyeAngles.y - anglesToDamage.y ) ) + + arrowData.arrow.SetAttachOffsetAngles( AnglesCompose( file.damageArrowAngles, anglesToDamage ) + < arrowData.randomAngle, 0, roll * 90.0 > ) + arrowData.damageDirection = Normalize( vecToDamage ) +} + +function UpdateDamageArrows( entity localViewPlayer, entity cockpit ) +{ + localViewPlayer.EndSignal( "OnDestroy" ) + cockpit.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( localViewPlayer ) + { + foreach ( arrowData in file.damageArrows ) + { + if ( IsValid( arrowData.arrow ) ) + { + arrowData.arrow.DisableDraw() + arrowData.arrow.ClearParent() + arrowData.attacker = null + arrowData.isVisible = false + arrowData.randomAngle = 0 + } + } + } + ) + + bool varsUpdated = false + + while ( true ) + { + WaitEndFrame() + + vector playerOrigin = localViewPlayer.GetOrigin() + + varsUpdated = false + bool inPhaseShift = localViewPlayer.IsPhaseShifted() + + foreach ( arrowData in file.damageArrows ) + { + if ( !arrowData.isVisible ) + { + continue + } + + if ( arrowData.grenade != null ) + { + if ( !IsValid( arrowData.grenade ) ) + arrowData.endTime = 0.0 + } + + if ( (file.damageArrowTime >= arrowData.endTime) || inPhaseShift ) + { + arrowData.arrow.DisableDraw() + arrowData.arrow.ClearParent() + arrowData.attacker = null + arrowData.isVisible = false + arrowData.randomAngle = 0 + continue + } + + if ( !varsUpdated ) // only call UpdateDamageArrowVars if one or more of the file.damageArrows is visible + { + varsUpdated = true + UpdateDamageArrowVars( localViewPlayer ) + } + + UpdateDamageArrowAngle( arrowData ) + + if ( !arrowData.isDying && ( ( arrowData.endTime - file.damageArrowTime ) <= file.damageArrowFadeDuration ) ) + { + arrowData.isDying = true + arrowData.arrow.Anim_NonScriptedPlay( DAMAGEARROW_FADEANIM ) + } + } + + wait( 0.0 ) + } +} diff --git a/Northstar.Client/mod/scripts/vscripts/client/cl_player.gnut b/Northstar.Client/mod/scripts/vscripts/client/cl_player.gnut new file mode 100644 index 000000000..fa5428c51 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/client/cl_player.gnut @@ -0,0 +1,2742 @@ +untyped + +global function ClPlayer_Init + +global function PlayIt +global function JumpRandomlyForever + +global function ClientCodeCallback_PlayerDidDamage +global function ClientCodeCallback_PlayerSpawned +//global function ClientCodeCallback_OnHudReloadScheme +global function ClientCodeCallback_HUDThink +global function Player_AddPlayer +global function Player_AddClient +global function PlayerConnectedOrDisconnected +global function ServerCallback_GameModeAnnouncement +global function MainHud_InitScoreBars + +global function ServerCallback_PlayerConnectedOrDisconnected +global function ClientCodeCallback_PlayerDisconnected +global function ServerCallback_PlayerChangedTeams +global function ClientCodeCallback_OnModelChanged +global function ServerCallback_RodeoerEjectWarning +global function ServerCallback_PlayScreenFXWarpJump +global function PlayShieldBreakEffect +global function PlayShieldActivateEffect +global function HandleDoomedState +global function RewardReadyMessage +global function TitanReadyMessage +global function CoreReadyMessage + +global function ServerCallback_RewardReadyMessage +global function ServerCallback_TitanReadyMessage + +global function OnClientPlayerAlive +global function OnClientPlayerDying +global function PlayPlayerDeathSound +global function StopPlayerDeathSound + +global function ServerCallback_ShowNextSpawnMessage +global function GetWaveSpawnTime +global function ServerCallback_HideNextSpawnMessage + +global function ClientCodeCallback_OnHealthChanged +global function ClientCodeCallback_OnCrosshairCurrentTargetChanged +global function Pressed_TitanNextMode +global function ClientCodeCallback_OnGib +global function ClientPilotSpawned +global function AddCallback_OnPlayerDisconnected + +global function IsPlayerEliminated + +global function ServerCallback_GiveMatchLossProtection +global function ServerCallback_OnEntityKilled +global function ServerCallback_OnTitanKilled + +global function ShouldShowSpawnAsTitanHint +global function ServerCallback_SetAssistInformation + +global function GetShieldEffectCurrentColor +global function ClientPlayerClassChanged + +#if DEV +global function BloodSprayDecals_Toggle +#endif + +const float DEFAULT_GAMEMODE_ANNOUNCEMENT_DURATION = 5.0 + +struct { + var orbitalstrike_tracer = null + var law_missile_tracer = null + float nextSpawnTime = 0.0 + entity lastEarnedReward // primarily used to check if we should still show the reward message after a delay +} file + +struct BloodDecalParams +{ + float traceDist + float secondaryTraceDist + asset fxType + asset secondaryFxType +} + +void function ClPlayer_Init() +{ + ClPilotJumpjet_Init() + ClDamageIndicator_Init() + + ClPlayer_Common_Precache() + + RegisterSignal( "OnAnimationDone" ) + RegisterSignal( "OnAnimationInterrupted" ) + RegisterSignal( "OnBleedingOut" ) + RegisterSignal( "PanelAlphaOverTime" ) + RegisterSignal( "LocalClientPlayerRespawned" ) + RegisterSignal( "OnClientPlayerAlive" ) + RegisterSignal( "OnClientPlayerDying" ) + RegisterSignal( "StopAlertCore" ) + RegisterSignal( "OnSpectatorMode" ) + RegisterSignal( "HealthChanged" ) + + FlagInit( "DamageDistancePrint" ) + FlagInit( "EnableTitanModeChange", true ) + FlagInit( "EnableBloodSprayDecals", true ) + + level.vduOpen <- false + level.canSpawnAsTitan <- false + level.grenadeIndicatorEnabled <- true + level.clientsLastKiller <- null + + AddCreateCallback( "player", SetupPlayerAnimEvents ) + AddCreateCallback( "player", MpClientPlayerInit ) + + AddCreateCallback( "first_person_proxy", SetupFirstPersonProxyEvents ) + AddCreateCallback( "predicted_first_person_proxy", SetupFirstPersonProxyEvents ) + + AddCreateCallback( "player", EnableDoDeathCallback ) + AddCreateCallback( "npc_titan", EnableDoDeathCallback ) + + AddCreateCallback( "titan_soul", CreateCallback_TitanSoul ) + + AddCallback_OnPlayerLifeStateChanged( PlayerADSDof ) + + file.orbitalstrike_tracer = PrecacheParticleSystem( $"Rocket_Smoke_Large" ) + //DEBUG Remove when bug is fixed. + file.law_missile_tracer = PrecacheParticleSystem( $"wpn_orbital_rocket_tracer" ) + + level.menuHideGroups <- {} + + level.spawnAsTitanSelected <- false + + AddPlayerFunc( Player_AddPlayer ) +} + +entity function FindEnemyRodeoParent( entity player ) +{ + entity ent = player.GetParent() + if ( ent == null ) + return null + + if ( !ent.IsTitan() ) + return null + + if ( ent == player.GetPetTitan() ) + return null + + if ( ent.GetTeam() == player.GetTeam() ) + return null + + return ent +} + +void function MpClientPlayerInit( entity player ) +{ + player.ClientCommand( "save_enable 0" ) +} + +void function ClientCodeCallback_PlayerSpawned( entity player ) +{ + if ( !IsValid( player ) ) + return + + if ( IsMenuLevel() ) + return + + ClearCrosshairPriority( crosshairPriorityLevel.ROUND_WINNING_KILL_REPLAY ) + + if ( !level.clientScriptInitialized ) + return + + // exists on server and client. Clear it when you respawn. + ClearRecentDamageHistory( player ) + DamageHistoryStruct blankDamageHistory + clGlobal.lastDamageHistory = blankDamageHistory + + if ( player == GetLocalViewPlayer() ) + { + foreach ( callbackFunc in clGlobal.onLocalViewPlayerSpawnedCallbacks ) + { + callbackFunc( player ) + } + } + + if ( player == GetLocalClientPlayer() ) + { + player.cv.lastSpawnTime = Time() + player.cv.roundSpawnCount++ + + foreach ( callbackFunc in clGlobal.onLocalClientPlayerSpawnedCallbacks ) + { + thread callbackFunc( player ) + } + } + + if ( player.IsTitan() ) + return + + if ( player.GetPlayerClass() == level.pilotClass ) + { + thread ClientPilotSpawned( player ) + } +} + + +void function ServerCallback_TitanReadyMessage() +{ + //thread TitanReadyMessage( 1.5, false ) //Delay was necessary at some point in time according to Brent, might no longer be true? + thread TitanReadyMessage( 0.0, false ) +} + + +void function ServerCallback_RewardReadyMessage( float timeSinceLastRespawn ) +{ + if ( timeSinceLastRespawn < 1.0 ) + thread RewardReadyMessage( 6.0, false ) + else + thread RewardReadyMessage( 0.0, false ) +} + + +void function RewardReadyMessage( float delay = 0.0, bool isQuick = false ) +{ + if ( delay > 0.0 ) + wait delay + + if ( !GamePlayingOrSuddenDeath() ) + return + + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) || IsSpectating() || IsWatchingKillReplay() ) + return + + if ( player.ContextAction_IsMeleeExecution() ) + return + + entity weapon = player.GetOffhandWeapon( OFFHAND_INVENTORY ) + if ( !IsValid( weapon ) ) + return + + file.lastEarnedReward = weapon + + if ( player.IsTitan() ) + { + EmitSoundOnEntity( player, "HUD_Boost_Card_Earned_1P" ) + + string rewardReadyMessage = expect string( GetWeaponInfoFileKeyField_WithMods_Global( weapon.GetWeaponClassName(), weapon.GetMods(), "readymessage" ) ) + string rewardReadyHint = expect string( GetWeaponInfoFileKeyField_WithMods_Global( weapon.GetWeaponClassName(), weapon.GetMods(), "readyhint" ) ) + asset rewardIcon = GetWeaponInfoFileKeyFieldAsset_WithMods_Global( weapon.GetWeaponClassName(), weapon.GetMods(), "hud_icon" ) + + AnnouncementData announcement = CreateAnnouncementMessageQuick( player, rewardReadyMessage, rewardReadyHint, <1, 0.5, 0>, rewardIcon ) + announcement.displayConditionCallback = LastEarnedRewardStillValid + AnnouncementFromClass( player, announcement ) + } + else + { + EmitSoundOnEntity( player, "HUD_Boost_Card_Earned_1P" ) + + string rewardReadyMessage = expect string( GetWeaponInfoFileKeyField_WithMods_Global( weapon.GetWeaponClassName(), weapon.GetMods(), "readymessage" ) ) + string rewardReadyHint = expect string( GetWeaponInfoFileKeyField_WithMods_Global( weapon.GetWeaponClassName(), weapon.GetMods(), "readyhint" ) ) + asset rewardIcon = GetWeaponInfoFileKeyFieldAsset_WithMods_Global( weapon.GetWeaponClassName(), weapon.GetMods(), "hud_icon" ) + + AnnouncementData announcement = CreateAnnouncementMessageQuick( player, rewardReadyMessage, rewardReadyHint, <1, 0.5, 0>, rewardIcon ) + announcement.displayConditionCallback = LastEarnedRewardStillValid + AnnouncementFromClass( player, announcement ) + } +} + +void function TitanReadyMessage( float delay = 0.0, bool isQuick = false ) +{ + if ( delay > 0.0 ) + wait delay + + if ( !GamePlayingOrSuddenDeath() ) + return + + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) || IsSpectating() || IsWatchingKillReplay() ) + return + + if ( player.ContextAction_IsMeleeExecution() ) + return + + if ( Riff_TitanAvailability() == eTitanAvailability.Never ) + return + + if ( !IsTitanAvailable( player ) && + (Riff_TitanAvailability() == eTitanAvailability.Custom) + ) + return + + int loadoutIndex = GetPersistentSpawnLoadoutIndex( player, "titan" ) + TitanLoadoutDef loadout = GetTitanLoadoutFromPersistentData( player, loadoutIndex ) + + string titanReadyMessage = GetTitanReadyMessageFromSetFile( loadout.setFile ) + string titanReadyHint = GetTitanReadyHintFromSetFile( loadout.setFile ) + + AnnouncementData announcement = CreateAnnouncementMessageQuick( player, titanReadyMessage, titanReadyHint, TEAM_COLOR_YOU, $"rui/hud/titanfall_marker_arrow_ready" ) + announcement.displayConditionCallback = ConditionNoTitan + AnnouncementFromClass( player, announcement ) + + #if FACTION_DIALOGUE_ENABLED + if ( !isQuick || CoinFlip() ) + PlayFactionDialogueOnLocalClientPlayer( "mp_titanReady" ) //Playing here as opposed to on the server since delay is normally not 0 + #endif + + if ( PlayerEarnMeter_GetMode( player ) == eEarnMeterMode.DEFAULT ) //Help stop spamming "Your Titan is Ready" + { + Cl_EarnMeter_SetLastHintTime( Time() ) + } +} + + +function CoreReadyMessage( entity player, bool isQuick = false ) +{ + if ( !GamePlayingOrSuddenDeath() ) + return + + if ( !IsAlive( player ) ) + return + + if ( GetDoomedState( player ) ) + return + + if ( !player.IsTitan() ) + return + + entity weapon = player.GetOffhandWeapon( OFFHAND_EQUIPMENT ) + + string coreOnlineMessage = expect string( weapon.GetWeaponInfoFileKeyField( "readymessage" ) ) + string coreOnlineHint = expect string( weapon.GetWeaponInfoFileKeyField( "readyhint" ) ) + + if ( isQuick ) + { + AnnouncementData announcement = CreateAnnouncementMessageQuick( player, coreOnlineMessage, coreOnlineHint, TEAM_COLOR_YOU ) + announcement.displayConditionCallback = ConditionPlayerIsTitan + AnnouncementFromClass( player, announcement ) + } + else + { + AnnouncementData announcement = CreateAnnouncementMessage( player, coreOnlineMessage, coreOnlineHint, TEAM_COLOR_YOU ) + announcement.displayConditionCallback = ConditionPlayerIsTitan + announcement.subText = coreOnlineHint + AnnouncementFromClass( player, announcement ) + } +} + + +bool function ConditionPlayerIsTitan() +{ + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) ) + return false + + return player.IsTitan() +} + + +bool function ConditionPlayerIsNotTitan() +{ + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) ) + return false + + return !player.IsTitan() +} + +bool function LastEarnedRewardStillValid() +{ + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) ) + return false + + entity weapon = player.GetOffhandWeapon( OFFHAND_INVENTORY ) + if ( !IsValid( weapon ) ) + return false + + return weapon == file.lastEarnedReward +} + + +bool function ConditionNoTitan() +{ + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) ) + return false + + if ( IsValid( player.GetPetTitan() ) ) + return false + + return !player.IsTitan() +} + + +function ClientPilotSpawned( entity player ) +{ + player.EndSignal( "SettingsChanged" ) + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) + + if ( (player != GetLocalViewPlayer()) ) + //Turning off for the time being since the front rodeo spot leaves persistent jumpjets in the face of the TItan + thread ParentedPlayerJets( player ) +} + +void function Player_AddClient( entity player ) +{ + if ( GetCurrentPlaylistVarInt( "titan_mode_change_allowed", 1 ) ) + RegisterConCommandTriggeredCallback( "+scriptCommand2", Pressed_TitanNextMode ) + + RegisterConCommandTriggeredCallback( "+scriptCommand2", Pressed_RequestTitanfall ) + RegisterConCommandTriggeredCallback( "+scriptCommand2", Pressed_ActivateMobilityGhost ) + + RegisterConCommandTriggeredCallback( "+use", Pressed_OfferRodeoBattery ) + RegisterConCommandTriggeredCallback( "+use", Pressed_RequestRodeoBattery ) + RegisterConCommandTriggeredCallback( "+useandreload", Pressed_OfferRodeoBattery ) + RegisterConCommandTriggeredCallback( "+useandreload", Pressed_RequestRodeoBattery ) + + #if MP + RegisterConCommandTriggeredCallback( "+use", Pressed_TryNukeGrenade ) + RegisterConCommandTriggeredCallback( "-use", Released_TryNukeGrenade ) + RegisterConCommandTriggeredCallback( "+useandreload", Pressed_TryNukeGrenade ) + RegisterConCommandTriggeredCallback( "-useandreload", Released_TryNukeGrenade ) + #endif + + Create_DamageIndicatorHUD() + + if ( !IsLobby() ) + { + player.EnableHealthChangedCallback() + + player.cv.deathTime <- 0.0 + player.cv.lastSpawnTime <- 0.0 + player.cv.deathOrigin <- <0.0, 0.0, 0.0> + player.cv.roundSpawnCount <- 0 + + thread CinematicIntroScreen() + } +} + +void function Player_AddPlayer( entity player ) +{ + player.s.weaponUpdateData <- {} + + player.s.trackedAttackers <- {} // for titans + player.classChanged = true +} + +function Pressed_RequestTitanfall( entity player ) +{ + if ( !IsTitanAvailable( player ) ) + return + + #if DEV + printt( player.GetEntIndex(), "Requested replacement Titan from eye pos " + player.EyePosition() + " view angles " + player.EyeAngles() + " player origin " + player.GetOrigin() + " map " + GetMapName() ) + #endif + + player.ClientCommand( "ClientCommand_RequestTitan" ) //Send client command regardless of whether we can call the titan in or not. Server decides + Rumble_Play( "rumble_titanfall_request", {} ) + + // + //if ( player.cv.announcementActive && player.cv.announcementActive.messageText == "#HUD_TITAN_READY" ) + //{ + // clGlobal.levelEnt.Signal( "AnnoucementPurge" ) + //} + // + ////PlayMusic( "Music_FR_Militia_TitanFall1" ) + //EmitSoundOnEntity( player, "titan_callin" ) + //return + //} +} + +function Pressed_TitanNextMode( entity player ) +{ + if ( player.IsTitan() ) + return + + if ( IsWatchingReplay() ) + return + + if ( !IsAlive( player ) ) + return + + if ( player.IsPhaseShifted() ) + return + + if ( !IsAlive( player.GetPetTitan() ) ) + return + + if ( !Flag( "EnableTitanModeChange" ) ) + return + + // cannot change modes while titan is incoming + if ( player.GetHotDropImpactTime() ) + return + + player.ClientCommand( "TitanNextMode" ) + + local newMode = player.GetPetTitanMode() + 1 + if ( newMode == eNPCTitanMode.MODE_COUNT ) + newMode = eNPCTitanMode.FOLLOW + + SetAutoTitanModeHudIndicator( player, newMode ) + + local guardModeAlias = GenerateTitanOSAlias( player, AUTO_TITAN_GUARD_MODE_DIAG_SUFFIX ) + local followModeAlias = GenerateTitanOSAlias( player, AUTO_TITAN_FOLLOW_MODE_DIAG_SUFFIX ) + + // prevent the sounds from stomping each other if button is pressed rapidly + StopSoundOnEntity( player, guardModeAlias ) + StopSoundOnEntity( player, AUTO_TITAN_GUARD_MODE_SOUND ) + StopSoundOnEntity( player, followModeAlias ) + StopSoundOnEntity( player, AUTO_TITAN_FOLLOW_MODE_SOUND ) + + if ( newMode == eNPCTitanMode.FOLLOW ) + { + EmitSoundOnEntity( player, followModeAlias ) + EmitSoundOnEntity( player, AUTO_TITAN_FOLLOW_MODE_SOUND ) + } + else if ( newMode == eNPCTitanMode.STAY ) + { + EmitSoundOnEntity( player, guardModeAlias ) + EmitSoundOnEntity( player, AUTO_TITAN_GUARD_MODE_SOUND ) + } +} + +/* +void function ClientCodeCallback_OnHudReloadScheme() +{ +} +*/ + +void function ClientCodeCallback_HUDThink() +{ + PerfStart( PerfIndexClient.ClientCodeCallback_HUDThink ) + + entity player = GetLocalViewPlayer() + + if ( !player.p.playerScriptsInitialized ) + { + PerfEnd( PerfIndexClient.ClientCodeCallback_HUDThink ) + return + } + + if ( !IsMenuLevel() ) + { + PerfStart( PerfIndexClient.ClientCodeCallback_HUDThink_4 ) + + ClGameState_Think() + PerfEnd( PerfIndexClient.ClientCodeCallback_HUDThink_4 ) + + PerfStart( PerfIndexClient.ClientCodeCallback_HUDThink_5 ) + UpdateVoiceHUD() + #if PC_PROG + UpdateChatHUDVisibility() + #endif // PC_PROG + PerfEnd( PerfIndexClient.ClientCodeCallback_HUDThink_5 ) + + UpdateScreenFade() + + entity clientPlayer = GetLocalClientPlayer() + if ( !IsWatchingKillReplay() && clientPlayer.classChanged ) + { + ClientPlayerClassChanged( clientPlayer, clientPlayer.GetPlayerClass() ) + } + + PerfStart( PerfIndexClient.ClientCodeCallback_HUDThink_6 ) + SmartAmmo_LockedOntoWarningHUD_Update() + WeaponFlyoutThink( player ) + PerfEnd( PerfIndexClient.ClientCodeCallback_HUDThink_6 ) + } + + PerfEnd( PerfIndexClient.ClientCodeCallback_HUDThink ) +} + +function ClientPlayerClassChanged( entity player, newClass ) +{ + //printl( "ClientPlayerClassChanged to " + player.GetPlayerClass() ) + player.classChanged = false + + level.vduOpen = false // vdu goes away when class changes + + Assert( !IsServer() ) + Assert( newClass, "No class " ) + + switch ( newClass ) + { + case "titan": + SetStandardAbilityBindingsForTitan( player ) + SetAbilityBinding( player, 6, "+offhand4", "-offhand4" ) // "+ability 6" + + LinkButtonPair( -1, -1, -1 ) + break + + case level.pilotClass: + SetStandardAbilityBindingsForPilot( player ) + SetAbilityBinding( player, 6, "+offhand4", "-offhand4" ) // "+ability 6" + + LinkButtonPair( IN_OFFHAND0, IN_OFFHAND1, IN_OFFHAND3 ) + + if ( clGlobal.isAnnouncementActive && (clGlobal.activeAnnouncement.messageText == "#HUD_CORE_ONLINE_STRYDER" || + clGlobal.activeAnnouncement.messageText == "#HUD_CORE_ONLINE_ATLAS" || + clGlobal.activeAnnouncement.messageText == "#HUD_CORE_ONLINE_OGRE" ) ) + { + clGlobal.levelEnt.Signal( "AnnoucementPurge" ) + } + break + + case "spectator": + LinkButtonPair( -1, -1, -1 ) + break + + default: + Assert( 0, "Unknown class " + newClass ) + } + + PlayActionMusic() +} + +function ShouldShowSpawnAsTitanHint( entity player ) +{ + if ( Time() - player.cv.deathTime < GetRespawnButtonCamTime( player ) ) + return false + + if ( GetGameState() < eGameState.Playing ) + return false + + if ( GetGameState() == eGameState.SwitchingSides ) + return false + + return !IsPlayerEliminated( player ) +} + +function ServerCallback_PlayerChangedTeams( player_eHandle, oldTeam, newTeam ) +{ + entity player = GetEntityFromEncodedEHandle( player_eHandle ) + if ( player == null ) + return + Assert( oldTeam != null ) + Assert( newTeam != null ) + + string playerName = player.GetPlayerNameWithClanTag() + vector playerNameColor = OBITUARY_COLOR_ENEMY + string teamString = "ENEMY" + if ( newTeam == GetLocalViewPlayer().GetTeam() ) + { + playerNameColor = OBITUARY_COLOR_FRIENDLY + teamString = "FRIENDLY" + } + + Obituary_Print( playerName, "CHANGED TEAMS TO", teamString, playerNameColor, OBITUARY_COLOR_WEAPON, playerNameColor ) + //"Switching " + player.GetPlayerNameWithClanTag() + " from " + GetTeamStr( team1 ) + " to " + GetTeamStr( team2 ) +} + +function ServerCallback_PlayerConnectedOrDisconnected( player_eHandle, state ) +{ + entity player = GetEntityFromEncodedEHandle( player_eHandle ) + PlayerConnectedOrDisconnected( player, state ) + + if ( !IsLobby() || !IsConnected() ) + UpdatePlayerStatusCounts() +} + +void function AddCallback_OnPlayerDisconnected( void functionref( entity ) callbackFunc ) +{ + Assert( !clGlobal.onPlayerDisconnectedFuncs.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnPlayerDisconnected" ) + + clGlobal.onPlayerDisconnectedFuncs.append( callbackFunc ) +} + +void function ClientCodeCallback_PlayerDisconnected( entity player, string cachedPlayerName ) +{ + PlayerConnectedOrDisconnected( player, 0, cachedPlayerName ) + + if ( ShouldUpdatePlayerStatusCounts() ) + UpdatePlayerStatusCounts() + + // Added via AddCallback_OnPlayerDisconnected + foreach ( callbackFunc in clGlobal.onPlayerDisconnectedFuncs ) + { + callbackFunc( player ) + } +} + +function ShouldUpdatePlayerStatusCounts() +{ + if ( GetGameState() < eGameState.WaitingForPlayers ) + return false + + if ( !IsLobby() ) + return true + + if ( !IsConnected() ) + return true + + return false +} + +function PlayerConnectedOrDisconnected( entity player, state, string disconnectingPlayerName = "" ) +{ + if ( IsLobby() || GetMapName() == "" ) + // HACK: If you are disconnecting GetMapName() in IsLobby() will return "" + return + + if ( !IsValid( player ) ) + return + + Assert( state == 0 || state == 1 ) + + if ( !IsValid( GetLocalViewPlayer() ) ) + return + + string playerName + if ( state == 0 ) + { + if ( disconnectingPlayerName == "" ) + return + + playerName = disconnectingPlayerName + Assert( typeof( playerName ) == "string" ) + } + else + { + playerName = player.GetPlayerNameWithClanTag() + Assert( typeof( playerName ) == "string" ) + } + + vector playerNameColor = player.GetTeam() == GetLocalViewPlayer().GetTeam() ? OBITUARY_COLOR_FRIENDLY : OBITUARY_COLOR_ENEMY + string connectionString = (state == 0) ? "#MP_PLAYER_DISCONNECTED" : "#MP_PLAYER_CONNECTED" + + Obituary_Print_Generic( connectionString, playerName, <255, 255, 255>, playerNameColor ) +} + +void function ClientCodeCallback_PlayerDidDamage( PlayerDidDamageParams params ) +{ + if ( IsWatchingThirdPersonKillReplay() ) + return + + entity attacker = GetLocalViewPlayer() + if ( !IsValid( attacker ) ) + return + + entity victim = params.victim + if ( !IsValid( victim ) ) + return + + vector damagePosition = params.damagePosition + int hitBox = params.hitBox + int damageType = params.damageType + float damageAmount = params.damageAmount + int damageFlags = params.damageFlags + int hitGroup = params.hitGroup + entity weapon = params.weapon + float distanceFromAttackOrigin = params.distanceFromAttackOrigin + + bool playHitSound = true + bool showCrosshairHitIndicator = true + bool hitIneffective = false + bool victimIsHeavyArmor = false + bool isCritShot = (damageType & DF_CRITICAL) ? true : false + bool isHeadShot = (damageType & DF_HEADSHOT) ? true : false + bool isKillShot = (damageType & DF_KILLSHOT) ? true : false + bool isMelee = (damageType & DF_MELEE) ? true : false + bool isExplosion = (damageType & DF_EXPLOSION) ? true : false + bool isBullet = (damageType & DF_BULLET) ? true : false + bool isShotgun = (damageType & DF_SHOTGUN) ? true : false + bool isDoomFatality = (damageType & DF_DOOM_FATALITY) ? true : false + bool isDoomProtected = ((damageType & DF_DOOM_PROTECTED) && !isDoomFatality) ? true : false + victimIsHeavyArmor = victim.GetArmorType() == ARMOR_TYPE_HEAVY + + isDoomFatality = false + isDoomProtected = false + + if ( isDoomProtected ) + RegisterDoomProtectionHintDamage( damageAmount ) + + bool playKillSound = isKillShot + + if ( !attacker.IsTitan() ) + { + if ( victimIsHeavyArmor ) + { + showCrosshairHitIndicator = true + if ( victim.IsTitan() ) + hitIneffective = false //!IsHitEffectiveVsTitan( victim, damageType ) + else + hitIneffective = isCritShot || isHeadShot || !IsHitEffectiveVsNonTitan( victim, damageType ) + } + else + { + switch ( victim.GetSignifierName() ) + { + case "npc_super_spectre": + //if ( !( damageType & DF_CRITICAL ) ) + // hitIneffective = true + + default: + if ( (damageType & DF_BULLET && damageType & DF_MAX_RANGE) ) + hitIneffective = true + break + } + } + } + else + { + if ( victim.IsTitan() && victim.IsPlayer() ) + { + if ( PlayerHasPassive( victim, ePassives.PAS_BERSERKER ) ) + hitIneffective = true + } + } + + if ( damageType & DF_MAX_RANGE && damageType & DF_BULLET ) + // TODO: this is crap; these damage types should just send DF_NO_HITBEEP + playHitSound = false + + if ( damageType & DF_TITAN_STEP ) + // TODO: this is crap; these damage types should just send DF_NO_HITBEEP + { + playHitSound = false + playKillSound = false + } + + if ( damageType & DF_MELEE ) + // TODO: this is crap; these damage types should just send DF_NO_HITBEEP + { + playHitSound = false + playKillSound = false + } + + if ( damageType & DF_NO_HITBEEP ) + { + playHitSound = false + playKillSound = false + } + + if ( damageFlags & DAMAGEFLAG_VICTIM_HAS_VORTEX ) + showCrosshairHitIndicator = false + + if ( damageType & DF_SHIELD_DAMAGE ) + { + PlayShieldHitEffect( params ) + showCrosshairHitIndicator = true + } + else if ( damageAmount <= 0 ) + { + playHitSound = false + playKillSound = false + showCrosshairHitIndicator = false + } + + if ( damageType & DF_NO_INDICATOR ) + { + playHitSound = false + playKillSound = false + showCrosshairHitIndicator = false + } + + if ( isDoomProtected ) + playHitSound = false + + if ( showCrosshairHitIndicator ) + { + Tracker_PlayerAttackedTarget( attacker, victim ) + + //if ( hitIneffective ) + // Crosshair_ShowHitIndicator( CROSSHAIR_HIT_INEFFECTIVE ) + //else + // Crosshair_ShowHitIndicator( CROSSHAIR_HIT_NORMAL ) + // + //if ( (isCritShot || isDoomFatality) && !isDoomProtected ) + // Crosshair_ShowHitIndicator( CROSSHAIR_HIT_CRITICAL ) + // + //if ( isHeadShot ) + // Crosshair_ShowHitIndicator( CROSSHAIR_HIT_HEADSHOT ) + + if ( IsMultiplayer() && !victim.IsTitan() && !victim.IsHologram() ) + PROTO_HitIndicatorEffect( attacker, victim, damagePosition, isHeadShot, isKillShot ) + + if ( isKillShot ) + KillShotBloodSpray( attacker, victim, damagePosition, isExplosion, isBullet, isShotgun ) + + if ( victim.IsTitan() && isKillShot ) + ClientScreenShake( 8, 10, 1, Vector( 0, 0, 0 ) ) + + BloodSprayDecals( attacker, victim, damagePosition, damageAmount, isHeadShot, isKillShot, isMelee, isExplosion, isBullet, isShotgun ) + + DamageFlyout( damageAmount, damagePosition, victim, isHeadShot || isCritShot, hitIneffective ) + } + + bool playedHitSound = false + if ( playHitSound ) + { + if ( isHeadShot ) + playedHitSound = PlayHeadshotConfirmSound( attacker, victim, isKillShot ) + else if ( playKillSound ) + playedHitSound = PlayKillshotConfirmSound( attacker, victim, damageType ) + } + + if ( IsSpectre( victim ) ) + { + if ( isHeadShot ) + victim.Signal( "SpectreGlowEYEGLOW" ) + } + + // Play a hit sound effect if we didn't play a kill shot sound, and other conditions are met + if ( playHitSound && IsAlive( victim ) && !playedHitSound ) + { + PlayHitSound( victim, attacker, damageFlags, isCritShot, victimIsHeavyArmor, isKillShot, hitGroup ) + } + + if ( PlayerHasPassive( attacker, ePassives.PAS_SMART_CORE ) && isKillShot ) + { + attacker.p.smartCoreKills++ + } + + foreach ( callback in clGlobal.onLocalPlayerDidDamageCallback ) + { + callback( attacker, victim, damagePosition, damageType ) + } +} + +void function PlayHitSound( entity victim, entity attacker, int damageFlags, bool isCritShot, bool victimIsHeavyArmor, bool isKillShot, int hitGroup ) +{ + if ( damageFlags & DAMAGEFLAG_VICTIM_INVINCIBLE ) + { + EmitSoundOnEntity( attacker, "Player.HitbeepInvincible" ) + } + else if ( damageFlags & DAMAGEFLAG_VICTIM_HAS_VORTEX ) + { + EmitSoundOnEntity( attacker, "Player.HitbeepVortex" ) + } + else if ( isCritShot && victimIsHeavyArmor ) + { + EmitSoundOnEntity( attacker, "titan_damage_crit" ) + } + else if ( isCritShot ) + { + EmitSoundOnEntity( attacker, "Player.Hitbeep_crit" ) + } + else + { + EmitSoundOnEntity( attacker, "Player.Hitbeep" ) + } +} + +function PROTO_HitIndicatorEffect( entity player, entity victim, vector damagePosition, bool isHeadShot, bool isKillShot ) +{ + int fxId + if ( isKillShot ) + fxId = GetParticleSystemIndex( $"P_ar_impact_pilot_kill" ) + else if ( isHeadShot && !isKillShot ) + return + // fxId = GetParticleSystemIndex( $"P_ar_impact_pilot_headshot" ) + else + return + // fxId = GetParticleSystemIndex( $"P_ar_impact_pilot" ) + + vector victimVelocity = victim.GetVelocity() + damagePosition += (Length( victimVelocity ) * 0.15) * Normalize( victimVelocity ) + vector fxOffset = damagePosition - victim.GetOrigin() + StartParticleEffectOnEntityWithPos( victim, fxId, FX_PATTACH_ABSORIGIN_FOLLOW, -1, damagePosition - victim.GetOrigin(), <0, 0, 0> ) +} + +void function KillShotBloodSpray( entity player, entity victim, vector damagePosition, bool isExplosion, bool isBullet, bool isShotgun ) +{ + if ( IsSoftenedLocale() ) + return + + if ( !victim.IsHuman() && !IsProwler( victim ) ) + return + + if ( victim.IsMechanical() ) + return + + if ( victim.IsHologram() ) + return + + if ( !isExplosion && !isBullet && !isShotgun ) + return + + int fxId = GetParticleSystemIndex( FX_KILLSHOT_BLOODSPRAY ) + + vector victimVelocity = victim.GetVelocity() + damagePosition += (Length( victimVelocity ) * 0.15) * Normalize( victimVelocity ) + StartParticleEffectOnEntityWithPos( victim, fxId, FX_PATTACH_ABSORIGIN_FOLLOW, -1, damagePosition - victim.GetOrigin(), <0, 0, 0> ) +} + +void function BloodSprayDecals( entity player, entity victim, vector damagePosition, float damageAmount, bool isHeadShot, bool isKillShot, bool isMelee, bool isExplosion, bool isBullet, bool isShotgun ) +{ + if ( IsSoftenedLocale() || !Flag( "EnableBloodSprayDecals" ) ) + return + + if ( !victim.IsHuman() && !IsProwler( victim ) ) + return + + if ( victim.IsMechanical() ) + return + + if ( victim.IsHologram() ) + return + + if ( !isMelee && !isExplosion && !isBullet && !isShotgun ) + return + + // in MP, too expensive to do on every shot + if ( IsMultiplayer() && !isKillShot ) + return + + thread BloodSprayDecals_Think( player, victim, damagePosition, damageAmount, isHeadShot, isKillShot, isMelee, isExplosion, isBullet, isShotgun ) +} + +void function BloodSprayDecals_Think( entity player, entity victim, vector damagePosition, float damageAmount, bool isHeadShot, bool isKillShot, bool isMelee, bool isExplosion, bool isBullet, bool isShotgun ) +{ + player.EndSignal( "OnDestroy" ) + victim.EndSignal( "OnDestroy" ) + + BloodDecalParams params = BloodDecal_GetParams( damageAmount, isHeadShot, isKillShot, isMelee, isExplosion, isBullet, isShotgun ) + float traceDist = params.traceDist + float secondaryTraceDist = params.secondaryTraceDist + asset fxType = params.fxType + asset secondaryFxType = params.secondaryFxType + + int fxId = GetParticleSystemIndex( fxType ) + + // PRIMARY TRACES + vector traceStart = damagePosition + vector traceFwd = player.GetViewVector() + + if ( isExplosion || isMelee ) + { + // for explosion/melee damage, use chest instead of actual damage position + int attachID = victim.LookupAttachment( "CHESTFOCUS" ) + traceStart = victim.GetAttachmentOrigin( attachID ) + + if ( isExplosion ) + traceFwd = AnglesToForward( victim.GetAngles() ) * -1 + } + + vector traceEnd = damagePosition + (traceFwd * traceDist) + //TraceResults traceResult = TraceLine( traceStart, traceEnd, victim, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + var deferredTrace_primary = DeferredTraceLineHighDetail( traceStart, traceEnd, victim, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + while( !IsDeferredTraceFinished( deferredTrace_primary ) ) + WaitFrame() + + TraceResults traceResult = GetDeferredTraceResult( deferredTrace_primary ) + + vector primaryTraceEndPos = traceResult.endPos + vector primaryTraceNormal = traceResult.surfaceNormal + //DebugDrawLine( traceStart, traceEnd, 255, 150, 0, true, 5 ) + //DebugDrawSphere( primaryTraceEndPos, 8.0, 255, 0, 0, true, 5 ) + + bool doGravitySplat = isMelee ? false : true + + if ( traceResult.fraction < 1.0 ) + { + vector normAng = VectorToAngles( traceResult.surfaceNormal ) + vector fxAng = AnglesCompose( normAng, < 90, 0, 0 > ) + + StartParticleEffectInWorld( fxId, primaryTraceEndPos, fxAng ) + //DebugDrawAngles( endPos, fxAng, 5 ) + } + else if ( doGravitySplat ) + { + // trace behind the guy on the ground and put a decal there + float gravitySplatBackTraceDist = 58.0 // how far behind the guy to put the gravity splat + float gravitySplatDownTraceDist = 100.0 // max dist vertically to try to trace and put a gravity splat + vector groundTraceStartPos = damagePosition + (traceFwd * gravitySplatBackTraceDist) + vector groundTraceEndPos = groundTraceStartPos - <0, 0, 100> + + var deferredTrace_gravitySplat = DeferredTraceLineHighDetail( groundTraceStartPos, groundTraceEndPos, victim, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + + while( !IsDeferredTraceFinished( deferredTrace_gravitySplat ) ) + WaitFrame() + + TraceResults downTraceResult = GetDeferredTraceResult( deferredTrace_gravitySplat ) + + if ( downTraceResult.fraction < 1.0 ) + { + //DebugDrawLine( groundTraceStartPos, downTraceResult.endPos, 255, 150, 0, true, 5 ) + //DebugDrawSphere( downTraceResult.endPos, 4.0, 255, 0, 0, true, 5 ) + + vector normAng = VectorToAngles( downTraceResult.surfaceNormal ) + vector fxAng = AnglesCompose( normAng, < 90, 0, 0 > ) + + //DebugDrawAngles( downTraceResult.endPos, fxAng, 5 ) + + StartParticleEffectInWorld( fxId, downTraceResult.endPos, fxAng ) + } + } + + // MP doesn't want secondaries, too expensive + if ( IsMultiplayer() ) + return + + // SECONDARY TRACES + array testVecs = [] + vector tempAng = VectorToAngles( traceFwd ) + + if ( isExplosion ) + { + // for explosions, different & more angles for secondary splatter + testVecs.append( AnglesToRight( tempAng ) ) + testVecs.append( AnglesToRight( tempAng ) * -1 ) + testVecs.append( traceFwd * -1 ) + testVecs.append( AnglesToUp( tempAng ) ) + testVecs.append( AnglesToUp( tempAng ) * -1 ) + } + else + { + // mostly to cover edge cases involving corners + vector traceRight = AnglesToRight( tempAng ) + vector traceLeft = traceRight * -1 + vector backLeft = (traceFwd + traceLeft) * 0.5 + vector backRight = (traceFwd + traceRight) * 0.5 + testVecs.append( backRight ) + testVecs.append( backLeft ) + + // add blood on the ground for these weapons too + if ( isBullet || isShotgun ) + testVecs.append( AnglesToUp( tempAng ) * -1 ) + } + + if ( !testVecs.len() ) + return + + array secondaryDeferredTraces = [] + foreach ( testVec in testVecs ) + { + vector secondaryTraceEnd = traceStart + (testVec * secondaryTraceDist) + var secondaryDeferredTrace = DeferredTraceLineHighDetail( traceStart, secondaryTraceEnd, victim, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + secondaryDeferredTraces.append( secondaryDeferredTrace ) + } + + int secondaryFxId = GetParticleSystemIndex( secondaryFxType ) + + float startTime = Time() + array processedResults = [] + while ( processedResults.len() < secondaryDeferredTraces.len() ) + { + WaitFrame() + + foreach ( deferredTrace in secondaryDeferredTraces ) + { + if ( processedResults.contains( deferredTrace ) ) + continue + + if ( !IsDeferredTraceFinished( deferredTrace ) ) + continue + + processedResults.append( deferredTrace ) + + TraceResults traceResult = GetDeferredTraceResult( deferredTrace ) + + if ( traceResult.fraction == 1.0 ) + continue + + // don't put secondaries on the same wall as the primary + vector secondaryTraceNormal = traceResult.surfaceNormal + if ( primaryTraceNormal == secondaryTraceNormal ) + continue + + vector normAng = VectorToAngles( secondaryTraceNormal ) + vector fxAng = AnglesCompose( normAng, < 90, 0, 0 > ) + + vector endPos = traceResult.endPos + //DebugDrawSphere( endPos, 4.0, 255, 0, 0, true, 5 ) + StartParticleEffectInWorld( secondaryFxId, endPos, fxAng ) + } + + // timeout if traces aren't returning + if ( Time() - startTime >= 0.3 ) + return + } +} + +BloodDecalParams function BloodDecal_GetParams( float damageAmount, bool isHeadShot, bool isKillShot, bool isMelee, bool isExplosion, bool isBullet, bool isShotgun ) +{ + // default: bullet damage + float traceDist = 175 + float secondaryTraceDist = 100 + asset fxType = FX_BLOODSPRAY_DECAL_SML + asset secondaryFxType = FX_BLOODSPRAY_DECAL_SML + + if ( isBullet ) + { + // HACK- shotguns report isBullet also + if ( isShotgun ) + { + //if ( isKillShot ) + // fxType = FX_BLOODSPRAY_DECAL_LRG + //else + fxType = FX_BLOODSPRAY_DECAL_MED + } + else + { + if ( isKillShot ) + fxType = FX_BLOODSPRAY_DECAL_MED + else + fxType = FX_BLOODSPRAY_DECAL_SML + + if ( damageAmount >= 200 ) + { + traceDist = 216 + fxType = FX_BLOODSPRAY_DECAL_LRG + secondaryFxType = FX_BLOODSPRAY_DECAL_MED + } + } + } + + else if ( isExplosion ) + { + secondaryTraceDist = traceDist + + float maxDmg = 100 + float medDmg = 75 + + if ( damageAmount >= maxDmg ) + { + fxType = FX_BLOODSPRAY_DECAL_LRG + secondaryFxType = FX_BLOODSPRAY_DECAL_LRG + } + else if ( damageAmount >= medDmg ) + { + fxType = FX_BLOODSPRAY_DECAL_LRG + secondaryFxType = FX_BLOODSPRAY_DECAL_MED + } + else if ( isKillShot ) + { + fxType = FX_BLOODSPRAY_DECAL_MED + secondaryFxType = FX_BLOODSPRAY_DECAL_MED + } + } + + else if ( isMelee ) + { + traceDist = 96 + + if ( isKillShot ) + fxType = FX_BLOODSPRAY_DECAL_MED + } + + // for kills, increase trace distance a bit + if ( isKillShot ) + { + traceDist = traceDist + (traceDist * 0.1) + secondaryTraceDist = secondaryTraceDist + (secondaryTraceDist * 0.1) + } + + BloodDecalParams params + params.traceDist = traceDist + params.secondaryTraceDist = secondaryTraceDist + params.fxType = fxType + params.secondaryFxType = secondaryFxType + return params +} + +#if DEV +string function BloodSprayDecals_Toggle() +{ + string returnStr = "" + + if ( Flag( "EnableBloodSprayDecals" ) ) + { + FlagClear( "EnableBloodSprayDecals" ) + returnStr = "Blood spray decals DISABLED" + } + else + { + FlagSet( "EnableBloodSprayDecals" ) + returnStr = "Blood spray decals ENABLED" + } + + return returnStr +} +#endif + +function ServerCallback_RodeoerEjectWarning( soulHandle, ejectTime ) +{ + entity soul = GetEntityFromEncodedEHandle( soulHandle ) + + if ( !IsValid( soul ) ) + return + + thread TitanEjectHatchSequence( soul, ejectTime ) +} + +function TitanEjectHatchSequence( soul, ejectTime ) +{ + expect entity( soul ) + + soul.EndSignal( "OnSoulTransfer" ) + soul.EndSignal( "OnTitanDeath" ) + soul.EndSignal( "OnDestroy" ) + + local effects = [] + + OnThreadEnd( + function() : ( effects ) + { + foreach ( effect in effects ) + { + if ( !EffectDoesExist( effect ) ) + continue + + EffectStop( effect, true, true ) + } + } + ) + + int boltCount = 6 + int fxID = GetParticleSystemIndex( $"xo_spark_bolt" ) + + for ( int index = 0; index < boltCount; index++ ) + { + entity titan = soul.GetTitan() + + WaitEndFrame() // so OnTitanDeath/Destroy can happen + + if ( !IsAlive( titan ) ) + return + + if ( !titan.IsTitan() ) + { + printt( "WARNING: " + titan + " is not a Titan!" ) + return + } + + int attachID = titan.LookupAttachment( "HATCH_BOLT" + (index + 1) ) + //printt( "attachID is " + attachID ) + vector boltOrgin = titan.GetAttachmentOrigin( attachID ) + vector boltAngles = titan.GetAttachmentAngles( attachID ) + vector launchVec = AnglesToForward( boltAngles ) * 500 + + CreateClientsideGib( $"models/industrial/bolt_tiny01.mdl", boltOrgin, boltAngles, launchVec, < 0, 0, 0 >, 3.0, 1000.0, 200.0 ) + int effect = PlayFXOnTag( titan, fxID, attachID ) + effects.append( effect ) + EmitSoundOnEntity( titan, "titan_bolt_loose" ) + + wait (ejectTime / boltCount) + } +} + +void function ServerCallback_OnEntityKilled( attackerEHandle, victimEHandle, int scriptDamageType, damageSourceId ) +{ + expect int( damageSourceId ) + + bool isHeadShot = (scriptDamageType & DF_HEADSHOT) > 0 + + entity victim = GetEntityFromEncodedEHandle( victimEHandle ) + entity attacker = attackerEHandle ? GetHeavyWeightEntityFromEncodedEHandle( attackerEHandle ) : null + entity localClientPlayer = GetLocalClientPlayer() + + if ( !IsValid( victim ) ) + return + + Signal( victim, "OnDeath" ) + + if ( victim == localClientPlayer ) + { + victim.cv.deathOrigin = victim.GetOrigin() + level.clientsLastKiller = attacker + } + + if ( damageSourceId == eDamageSourceId.indoor_inferno ) + { + if ( victim == localClientPlayer ) + thread PlayerFieryDeath( victim ) + } + + UpdatePlayerStatusCounts() + + if ( IsValid( attacker ) && attacker.IsPlayer() ) + { + PlayTargetEliminatedTitanVO( attacker, victim ) + + if ( attacker == GetLocalViewPlayer() ) + WeaponFlyoutRefresh() // refreshes to display xp gained from kills + } + else if ( victim.IsPlayer() ) + { + if ( ("latestAssistTime" in victim.s) && victim.s.latestAssistTime >= Time() - MAX_NPC_KILL_STEAL_PREVENTION_TIME ) + { + attacker = expect entity( victim.s.latestAssistPlayer ) + damageSourceId = expect int( victim.s.latestAssistDamageSource ) + } + } + + if ( victim.IsPlayer() && victim != attacker ) + { + if ( attacker == localClientPlayer ) + { + thread PlayKillConfirmedSound( "Pilot_Killed_Indicator" ) + } + else if ( IsValid( attacker ) && attacker.IsTitan() ) + { + entity bossPlayer = attacker.GetBossPlayer() + if ( bossPlayer && bossPlayer == localClientPlayer ) + thread PlayKillConfirmedSound( "Pilot_Killed_Indicator" ) + } + } + else if ( (IsGrunt( victim ) || IsSpectre( victim )) && attacker == localClientPlayer ) + { + thread PlayKillConfirmedSound( "HUD_Grunt_Killed_Indicator" ) + } + + //if it's an auto titan, the obit was already printed when doomed + if ( (victim.IsTitan()) && (!victim.IsPlayer()) ) + return + + Obituary( attacker, "", victim, scriptDamageType, damageSourceId, isHeadShot ) +} + + +const float KILL_CONFIRM_DEBOUNCE = 0.025 +void function PlayKillConfirmedSound( string sound ) +{ + while ( true ) + { + if ( Time() - clGlobal.lastKillConfirmTime > KILL_CONFIRM_DEBOUNCE ) + { + clGlobal.lastKillConfirmTime = Time() + EmitSoundOnEntity( GetLocalClientPlayer(), sound ) + return + } + + WaitFrame() + } +} + +void function ServerCallback_OnTitanKilled( int attackerEHandle, int victimEHandle, int scriptDamageType, int damageSourceId ) +{ + //Gets run on every client whenever a titan is doomed by another player + bool isHeadShot = false + entity attacker = attackerEHandle != -1 ? GetEntityFromEncodedEHandle( attackerEHandle ) : null + entity victim = GetEntityFromEncodedEHandle( victimEHandle ) + + if ( (!IsValid( victim )) || (!IsValid( attacker )) ) + return + + //Obit: titans get scored/obits when doomed, so we don't want to just see "Player" in the obit, we want to see "Player's Titan" + bool victimIsOwnedTitan = victim.IsPlayer() + Obituary( attacker, "", victim, scriptDamageType, damageSourceId, isHeadShot, victimIsOwnedTitan ) +} + +function PlayTargetEliminatedTitanVO( attacker, victim ) +{ + entity localPlayer = GetLocalViewPlayer() + + if ( attacker != localPlayer ) + return + + if ( !victim.IsPlayer() ) + return + + if ( victim.IsTitan() ) + { + // a bit more delay for a titan explosion to clear + thread TitanCockpit_PlayDialogDelayed( localPlayer, 1.3, "elimTarget" ) + } + else + { + thread TitanCockpit_PlayDialogDelayed( localPlayer, 0.8, "elimEnemyPilot" ) + } +} + +function ServerCallback_SetAssistInformation( damageSourceId, attackerEHandle, entityEHandle, assistTime ) +{ + local ent = GetHeavyWeightEntityFromEncodedEHandle( entityEHandle ) + if ( !ent ) + return + + local latestAssistPlayer = GetEntityFromEncodedEHandle ( attackerEHandle ) + if ( !("latestAssistPlayer" in ent.s) ) + { + ent.s.latestAssistPlayer <- latestAssistPlayer + ent.s.latestAssistDamageSource <- damageSourceId + ent.s.latestAssistTime <- assistTime + } + else + { + ent.s.latestAssistPlayer = latestAssistPlayer + ent.s.latestAssistDamageSource = damageSourceId + ent.s.latestAssistTime = assistTime + } +} + +void function ClientCodeCallback_OnModelChanged( entity ent ) +{ +/* + // OnModelChanged gets called for each model change, but gets processed after the model has done all switches + + if ( !IsValid( ent ) ) + return; + + if ( !("creationCount" in ent.s) ) + return; + + Assert( ent instanceof C_BaseAnimating ); +*/ +} + + +void function ClientCodeCallback_OnHealthChanged( entity ent, int oldHealth, int newHealth ) +{ + if ( IsLobby() ) + return + + entity player = GetLocalViewPlayer() + if ( !IsValid( player ) ) + return + + if ( !IsValid( ent ) ) + return + + ent.Signal( "HealthChanged", { oldHealth = oldHealth, newHealth = newHealth } ) +} + +void function ClientCodeCallback_OnCrosshairCurrentTargetChanged( entity player, entity newTarget ) +{ + if ( IsLobby() ) + return; + if ( !IsValid( player ) ) + return + + if ( IsValid( newTarget ) ) + TryOfferRodeoBatteryHint( newTarget ) +} + +void function SetupPlayerAnimEvents( entity player ) +{ + SetupPlayerJumpJetAnimEvents( player ) + AddAnimEvent( player, "WallHangAttachDataKnife", WallHangAttachDataKnife ) +} + +void function JumpRandomlyForever() +{ + for (;; ) + { + if ( IsWatchingReplay() ) + { + wait 1 + continue + } + + entity player = GetLocalClientPlayer() + if ( !IsAlive( player ) || player != GetLocalViewPlayer() ) + { + wait 1 + continue + } + + printt( "jump!" ) + player.ClientCommand( "+jump" ) + wait 0 + player.ClientCommand( "-jump" ) + + wait RandomFloatRange( 0.2, 1.1 ) + } +} + +void function RemoteTurretFadeoutAnimEvent( entity ent ) +{ + entity player = GetLocalViewPlayer() + ScreenFade( player, 0, 0, 0, 255, 0.1, 0.25, FFADE_OUT ); +} + +void function SetupFirstPersonProxyEvents( entity firstPersonProxy ) +{ + //printt( "SetupFirstPersonProxyEvents" ) + + AddAnimEvent( firstPersonProxy, "mantle_smallmantle", OnSmallMantle ) + AddAnimEvent( firstPersonProxy, "mantle_mediummantle", OnMediumMantle ) + AddAnimEvent( firstPersonProxy, "mantle_lowmantle", OnLowMantle ) + AddAnimEvent( firstPersonProxy, "mantle_extralowmantle", OnExtraLowMantle ) + AddAnimEvent( firstPersonProxy, "remoteturret_fadeout", RemoteTurretFadeoutAnimEvent ) +} + +void function OnSmallMantle( entity firstPersonProxy ) //Was set up in script instead of anim to be able to play quieter sounds with stealth passive. No longer needed, but more work to move inside of anim +{ + entity player = GetLocalViewPlayer() + EmitSoundOnEntity( firstPersonProxy, "mantle_smallmantle" ) +} + +void function OnMediumMantle( entity firstPersonProxy ) //Was set up in script instead of anim to be able to play quieter sounds with stealth passive. No longer needed, but more work to move inside of anim +{ + entity player = GetLocalViewPlayer() + EmitSoundOnEntity( firstPersonProxy, "mantle_mediummantle" ) +} + +void function OnLowMantle( entity firstPersonProxy ) //Was set up in script instead of anim to be able to play quieter sounds with stealth passive. No longer needed, but more work to move inside of anim +{ + entity player = GetLocalViewPlayer() + EmitSoundOnEntity( firstPersonProxy, "mantle_lowmantle" ) +} + +void function OnExtraLowMantle( entity firstPersonProxy ) //Was set up in script instead of anim to be able to play quieter sounds with stealth passive. No longer needed, but more work to move inside of anim +{ + entity player = GetLocalViewPlayer() + EmitSoundOnEntity( firstPersonProxy, "mantle_extralow" ) +} + +void function CreateCallback_TitanSoul( entity ent ) +{ +} + +bool function ShouldHideRespawnSelectionText( entity player ) +{ + if ( player != GetLocalClientPlayer() ) + return false + if ( player.GetPlayerClass() != "spectator" ) + return false + if ( IsWatchingReplay() ) + return false + + return true +} + + + +void function WallHangAttachDataKnife( entity player ) +{ + int attachIdx = player.LookupAttachment( "l_hand" ) + if ( attachIdx == 0 ) + // hack while i wait for the attachment to be fixed + return + + entity dataknife = CreateClientSidePropDynamic( player.GetAttachmentOrigin( attachIdx ), player.GetAttachmentAngles( attachIdx ), DATA_KNIFE_MODEL ) + dataknife.SetParent( player, "l_hand" ) + + thread DeleteDataKnifeAfterWallHang( player, dataknife ) +} + +void function DeleteDataKnifeAfterWallHang( entity player, entity dataknife ) +{ + OnThreadEnd( + function() : ( dataknife ) + { + if ( IsValid( dataknife ) ) + dataknife.Kill_Deprecated_UseDestroyInstead() + } + ) + + player.EndSignal( "OnDeath" ) + player.EndSignal( "OnDestroy" ) + + for (;; ) + { + Wait( 0.1 ) + if ( !player.IsWallHanging() ) + break + } +} + +bool function ClientCodeCallback_OnGib( entity victim, vector attackDir ) +{ + if ( !victim.IsMechanical() ) + return SpawnFleshGibs( victim, attackDir ) + + return false +} + +bool function SpawnFleshGibs( entity victim, vector attackDir ) +{ + asset modelName = $"models/gibs/human_gibs.mdl" + attackDir = Normalize( attackDir ) + + float cullDist = 2048.0 + if ( "gibDist" in victim.s ) + cullDist = expect float( victim.s.gibDist ) + + vector startOrigin = victim.GetWorldSpaceCenter() + (attackDir * -30) + + vector origin = victim.GetOrigin() + < RandomIntRange( 10, 20 ), RandomIntRange( 10, 20 ), RandomIntRange( 32, 64 ) > + vector angles = < 0, 0, 0 > + vector flingDir = attackDir * RandomIntRange( 80, 200 ) + + int fxID + bool isSoftenedLocale = IsSoftenedLocale() + + if ( isSoftenedLocale ) + { + if ( victim.GetModelName() == FLYER_MODEL ) + fxID = StartParticleEffectOnEntity( victim, GetParticleSystemIndex( $"death_pinkmist_LG_nochunk" ), FX_PATTACH_ABSORIGIN_FOLLOW, 0 ) + else + fxID = StartParticleEffectOnEntity( victim, GetParticleSystemIndex( $"death_pinkmist_nochunk" ), FX_PATTACH_ABSORIGIN_FOLLOW, 0 ) + } + else + { + if ( victim.GetModelName() == FLYER_MODEL ) + fxID = StartParticleEffectOnEntity( victim, GetParticleSystemIndex( $"death_pinkmist_LG" ), FX_PATTACH_ABSORIGIN_FOLLOW, 0 ) + else + fxID = StartParticleEffectOnEntity( victim, GetParticleSystemIndex( $"death_pinkmist" ), FX_PATTACH_ABSORIGIN_FOLLOW, 0 ) + } + + EffectSetControlPointVector( fxID, 1, flingDir ) + + if ( isSoftenedLocale ) + return true + + vector angularVel = < 0, 0, 0 > + float lifeTime = 10.0 + CreateClientsideGibWithBodyGroupGibs( modelName, victim.GetOrigin(), angles, attackDir, angularVel, lifeTime, cullDist, 1024 ) + + return true +} + +function ServerCallback_PlayScreenFXWarpJump() +{ + if ( IsWatchingReplay() ) + return false + + thread PlayScreenFXWarpJump( GetLocalClientPlayer() ) +} + +void function PlayScreenFXWarpJump( entity clientPlayer ) +{ + clientPlayer.EndSignal( "OnDeath" ) + clientPlayer.EndSignal( "OnDestroy" ) + + entity player = GetLocalViewPlayer() + int index = GetParticleSystemIndex( SCREENFX_WARPJUMP ) + int indexD = GetParticleSystemIndex( SCREENFX_WARPJUMPDLIGHT ) + int fxID = StartParticleEffectInWorldWithHandle( index, < 0, 0, 0 >, < 0, 0, 0 > ) + int fxID2 = -1 + if ( IsValid( player.GetCockpit() ) ) + { + fxID2 = StartParticleEffectOnEntity( player, indexD, FX_PATTACH_POINT_FOLLOW, player.GetCockpit().LookupAttachment( "CAMERA" ) ) + EffectSetIsWithCockpit( fxID2, true ) + } + + OnThreadEnd( + function() : ( clientPlayer, fxID, fxID2 ) + { + if ( IsValid( clientPlayer ) && !IsAlive( clientPlayer ) ) + { + EffectStop( fxID, true, false ) + if ( fxID2 > -1 ) + EffectStop( fxID2, true, false ) + } + } + ) + + wait 3.2 + if ( IsValid( player.GetCockpit() ) ) + thread TonemappingUpdateAfterWarpJump() +} + +const EXPOSURE_RAMPDOWN_DURATION = 2 +const EXPOSURE_RAMPDOWN_MAX = 20 +const EXPOSURE_RAMPDOWN_MIN = 0 +const MAX_RAMPDOWN_DURATION = 5 +const MAX_RAMPDOWN_MAX = 3 +const MAX_RAMPDOWN_MIN = 1 + +function TonemappingUpdateAfterWarpJump() +{ + // Turn cubemaps black inside drop ship, since it's pretty dark in there anyway and we don't have a great way to take a valid cubemap shot for that location. + SetConVarFloat( "mat_envmap_scale", 0 ); + + AutoExposureSetMaxExposureMultiplier( 500 ); // allow exposure to actually go bright, even if it's clamped in the level. + + // Start the exposure super bright behind the white FX, and ramp it down quickly to normal. + local startTime = Time() + while( 1 ) + { + local time = Time() - startTime + float factor = GraphCapped( time, 0, EXPOSURE_RAMPDOWN_DURATION, 1, 0 ) + local toneMapScale = EXPOSURE_RAMPDOWN_MIN + (EXPOSURE_RAMPDOWN_MAX - EXPOSURE_RAMPDOWN_MIN) * factor * factor * factor * factor + AutoExposureSetExposureCompensationBias( toneMapScale ) + AutoExposureSnap() + wait 0 + if ( factor == 0 ) + break; + } + + // Ramp the max exposure multiplier back down to 1 gently + startTime = Time() + while( 1 ) + { + local time = Time() - startTime + float factor = GraphCapped( time, 0, MAX_RAMPDOWN_DURATION, 1, 0 ) + local scale = MAX_RAMPDOWN_MIN + (MAX_RAMPDOWN_MAX - MAX_RAMPDOWN_MIN) * factor * factor + AutoExposureSetMaxExposureMultiplier( scale ); + wait 0 + if ( factor == 0 ) + break; + } +} + +function SetPanelAlphaOverTime( panel, alpha, duration ) +{ + // HACK this should be a code command - Mackey + Signal( panel, "PanelAlphaOverTime" ) + EndSignal( panel, "PanelAlphaOverTime" ) + EndSignal( panel, "OnDestroy" ) + + local startTime = Time() + local endTime = startTime + duration + local startAlpha = panel.GetPanelAlpha() + + while( Time() <= endTime ) + { + float a = GraphCapped( Time(), startTime, endTime, startAlpha, alpha ) + panel.SetPanelAlpha( a ) + WaitFrame() + } + + panel.SetPanelAlpha( alpha ) +} + + + +function HandleDoomedState( entity player, entity titan ) +{ + bool isDoomed = GetDoomedState( titan ) + if ( isDoomed ) + { + titan.Signal( "Doomed" ) + + if ( HasSoul( titan ) ) + { + entity soul = titan.GetTitanSoul() + soul.Signal( "Doomed" ) + } + } +} + +const asset SHIELD_BREAK_FX = $"P_xo_armor_break_CP" +function PlayShieldBreakEffect( entity ent ) +{ + entity shieldEnt = ent + if ( IsSoul( ent ) ) + { + shieldEnt = ent.GetTitan() + if ( !shieldEnt ) + return + } + + float shieldHealthFrac = GetShieldHealthFrac( shieldEnt ) + + int shieldBreakFX = GetParticleSystemIndex( SHIELD_BREAK_FX ) + + local attachID + if ( shieldEnt.IsTitan() ) + attachID = shieldEnt.LookupAttachment( "exp_torso_main" ) + else + attachID = shieldEnt.LookupAttachment( "ref" ) // TEMP + + local shieldFXHandle = StartParticleEffectOnEntity( shieldEnt, shieldBreakFX, FX_PATTACH_POINT_FOLLOW, attachID ) + EffectSetControlPointVector( shieldFXHandle, 1, GetShieldEffectCurrentColor( 1 - shieldHealthFrac ) ) +} + +function PlayShieldActivateEffect( entity ent ) +{ + entity shieldEnt = ent + if ( IsSoul( ent ) ) + { + shieldEnt = ent.GetTitan() + if ( !shieldEnt ) + return + } + + float shieldHealthFrac = GetShieldHealthFrac( shieldEnt ) + + int shieldBreakFX = GetParticleSystemIndex( SHIELD_BREAK_FX ) + + local attachID + if ( shieldEnt.IsTitan() ) + attachID = shieldEnt.LookupAttachment( "exp_torso_main" ) + else + attachID = shieldEnt.LookupAttachment( "ref" ) // TEMP + + local shieldFXHandle = StartParticleEffectOnEntity( shieldEnt, shieldBreakFX, FX_PATTACH_POINT_FOLLOW, attachID ) + EffectSetControlPointVector( shieldFXHandle, 1, GetShieldEffectCurrentColor( 1 - shieldHealthFrac ) ) +} + +function PlayIt( entity victim ) +{ + float shieldHealthFrac = GetShieldHealthFrac( victim ) + + int shieldbodyFX = GetParticleSystemIndex( SHIELD_BODY_FX ) + local attachID + if ( victim.IsTitan() ) + attachID = victim.LookupAttachment( "exp_torso_main" ) + else + attachID = victim.LookupAttachment( "ref" ) // TEMP + + local shieldFXHandle = StartParticleEffectOnEntity( victim, shieldbodyFX, FX_PATTACH_POINT_FOLLOW, attachID ) + + EffectSetControlPointVector( shieldFXHandle, 1, GetShieldEffectCurrentColor( 1 - shieldHealthFrac ) ) +} + +function PlayShieldHitEffect( PlayerDidDamageParams params ) +{ + entity player = GetLocalViewPlayer() + entity victim = params.victim + //vector damagePosition = params.damagePosition + //int hitBox = params.hitBox + //int damageType = params.damageType + //float damageAmount = params.damageAmount + //int damageFlags = params.damageFlags + //int hitGroup = params.hitGroup + //entity weapon = params.weapon + //float distanceFromAttackOrigin = params.distanceFromAttackOrigin + + //shieldFX <- GetParticleSystemIndex( SHIELD_FX ) + //StartParticleEffectInWorld( shieldFX, damagePosition, player.GetViewVector() * -1 ) + + float shieldHealthFrac = GetShieldHealthFrac( victim ) + + int shieldbodyFX = GetParticleSystemIndex( SHIELD_BODY_FX ) + local attachID + if ( victim.IsTitan() ) + attachID = victim.LookupAttachment( "exp_torso_main" ) + else + attachID = victim.LookupAttachment( "ref" ) // TEMP + + local shieldFXHandle = StartParticleEffectOnEntity( victim, shieldbodyFX, FX_PATTACH_POINT_FOLLOW, attachID ) + + EffectSetControlPointVector( shieldFXHandle, 1, GetShieldEffectCurrentColor( 1 - shieldHealthFrac ) ) +} + +const table SHIELD_COLOR_CHARGE_FULL = { r = 115, g = 247, b = 255 } // blue +const table SHIELD_COLOR_CHARGE_MED = { r = 200, g = 128, b = 80 } // orange +const table SHIELD_COLOR_CHARGE_EMPTY = { r = 200, g = 80, b = 80 } // red + +const SHIELD_COLOR_CROSSOVERFRAC_FULL2MED = 0.75 // from zero to this fraction, fade between full and medium charge colors +const SHIELD_COLOR_CROSSOVERFRAC_MED2EMPTY = 0.95 // from "full2med" to this fraction, fade between medium and empty charge colors + +function GetShieldEffectCurrentColor( shieldHealthFrac ) +{ + local color1 = SHIELD_COLOR_CHARGE_FULL + local color2 = SHIELD_COLOR_CHARGE_MED + local color3 = SHIELD_COLOR_CHARGE_EMPTY + + local crossover1 = SHIELD_COLOR_CROSSOVERFRAC_FULL2MED // from zero to this fraction, fade between color1 and color2 + local crossover2 = SHIELD_COLOR_CROSSOVERFRAC_MED2EMPTY // from crossover1 to this fraction, fade between color2 and color3 + + local colorVec = < 0, 0, 0 > + // 0 = full charge, 1 = no charge remaining + if ( shieldHealthFrac < crossover1 ) + { + colorVec.x = Graph( shieldHealthFrac, 0, crossover1, color1.r, color2.r ) + colorVec.y = Graph( shieldHealthFrac, 0, crossover1, color1.g, color2.g ) + colorVec.z = Graph( shieldHealthFrac, 0, crossover1, color1.b, color2.b ) + } + else if ( shieldHealthFrac < crossover2 ) + { + colorVec.x = Graph( shieldHealthFrac, crossover1, crossover2, color2.r, color3.r ) + colorVec.y = Graph( shieldHealthFrac, crossover1, crossover2, color2.g, color3.g ) + colorVec.z = Graph( shieldHealthFrac, crossover1, crossover2, color2.b, color3.b ) + } + else + { + // for the last bit of overload timer, keep it max danger color + colorVec.x = color3.r + colorVec.y = color3.g + colorVec.z = color3.b + } + + return colorVec +} + + + +void function PlayPlayerDeathSound( entity player ) +{ + if ( IsPlayerEliminated( player ) ) + EmitSoundOnEntity( player, "player_death_begin_elimination" ) + else + EmitSoundOnEntity( player, "Player_Death_Begin" ) +} + +void function StopPlayerDeathSound( entity player ) +{ + StopSoundOnEntity( player, "Player_Death_Begin" ) + EmitSoundOnEntity( player, "Player_Death_PrespawnTransition" ) +} + +function OnClientPlayerAlive( entity player ) +{ + player.Signal( "OnClientPlayerAlive" ) // TEMP; this should not be necessary, but IsWatchingKillReplay is wrong + player.EndSignal( "OnClientPlayerAlive" ) + + UpdateClientHudVisibility( player ) + + if ( IsWatchingReplay() ) + return + + if ( GetGameState() < eGameState.Playing ) + return +} + +function OnClientPlayerDying( entity player ) +{ + player.Signal( "OnClientPlayerDying" ) // TEMP; this should not be necessary, but IsWatchingKillReplay is wrong + player.EndSignal( "OnClientPlayerDying" ) + + entity player = GetLocalClientPlayer() + UpdateClientHudVisibility( player ) +// thread ShowDeathRecap( player ) + + if ( IsWatchingReplay() ) + return + + player.cv.deathTime = Time() + + thread DeathCamCheck( player ) +} + +void function ShowDeathRecap( entity player ) +{ + Assert( player == GetLocalClientPlayer() ) + + DisableCallingCardEvents() + + if ( player.e.recentDamageHistory.len() == 0 ) + return + + DamageHistoryStruct damageHistory = player.e.recentDamageHistory[ 0 ] + + entity attacker = damageHistory.attacker + + if ( !IsValid( attacker ) ) + return + + EndSignal( attacker, "OnDestroy" ) + + if ( !attacker.IsPlayer() ) + return + + if ( attacker.GetTeam() == player.GetTeam() ) + return + + wait( 1.0 ) + + CallsignEvent( eCallSignEvents.YOU, attacker, Localize( "#DEATH_SCREEN_KILLED_YOU" ) ) +} + +void function HideDeathRecap( entity player, var rui ) +{ + float minDisplayTime = 6.0 + float startTime = Time() + + waitthread DeathRecapHideDelay( player ) + wait( 0.5 ) + + float elapsedTime = Time() - startTime + if ( elapsedTime < minDisplayTime ) + wait( minDisplayTime - elapsedTime ) + + RuiSetBool( rui, "playOutro", true ) + RuiSetGameTime( rui, "outroStartTime", Time() ) + + EnableCallingCardEvents() +} + +void function DeathRecapHideDelay( entity player ) +{ + EndSignal( clGlobal.levelEnt, "LocalClientPlayerRespawned" ) + EndSignal( clGlobal.levelEnt, "OnSpectatorMode" ) + + WaitForever() +} + +void function DeathCamCheck( entity player ) +{ + wait GetRespawnButtonCamTime( player ) +} + +void function ServerCallback_ShowNextSpawnMessage( float nextSpawnTime ) +{ + entity player = GetLocalClientPlayer() + float camTime = GetRespawnButtonCamTime( player ) + + file.nextSpawnTime = nextSpawnTime + + if ( nextSpawnTime > Time() + camTime ) + thread ShowSpawnDelayMessage( nextSpawnTime ) +} + + +void function ShowSpawnDelayMessage( nextSpawnTime ) +{ + float waitTime = max( nextSpawnTime - Time(), 0 ) + + if ( waitTime < 1.0 ) + return + + entity player = GetLocalClientPlayer() + + //player.cv.nextSpawnTimeLabel.SetAlpha( 255 ) + //player.cv.nextSpawnTimeLabel.Show() + //player.cv.nextSpawnTimeLabel.SetAutoText( "#GAMEMODE_DEPLOYING_IN_N", HATT_GAME_COUNTDOWN_SECONDS, nextSpawnTime ) + // + //if ( !player.cv.nextSpawnTimeLabel.IsAutoText() ) + // player.cv.nextSpawnTimeLabel.EnableAutoText() + + while ( !IsAlive( player ) && waitTime > 0.0 ) + { + waitTime = max( nextSpawnTime - Time(), 0 ) + + AddPlayerHint( waitTime, 0.25, $"", "#GAMEMODE_DEPLOYING_IN_N", int( waitTime ) ) + + wait 1.0 + } +} +void function ServerCallback_HideNextSpawnMessage() +{ + entity player = GetLocalClientPlayer() + + HidePlayerHint( "#GAMEMODE_DEPLOYING_IN_N" ) +} + +float function GetWaveSpawnTime() +{ + return (file.nextSpawnTime) +} + +bool function IsPlayerEliminated( entity player ) +{ + return (player.GetPlayerGameStat( PGS_ELIMINATED ) > 0) +} + +function PlayerFieryDeath( player ) +{ + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnClientPlayerAlive" ) + clGlobal.levelEnt.EndSignal( "OnSpectatorMode" ) + + local offset = < 0, 0, 0 > + if ( player.IsTitan() ) + offset = < 0, 0, 96 > + + entity scriptRef = CreatePropDynamic( $"models/dev/empty_model.mdl", player.GetOrigin() + offset, player.GetAngles() ) + scriptRef.SetParent( player ) + + local fxHandle = StartParticleEffectOnEntity( scriptRef, GetParticleSystemIndex( $"P_burn_player" ), FX_PATTACH_ABSORIGIN_FOLLOW, -1 ) + + OnThreadEnd( + function () : ( fxHandle, scriptRef ) + { + EffectStop( fxHandle, false, false ) + if ( IsValid( scriptRef ) ) + scriptRef.Destroy() + } + ) + WaitForever() +} + + +function ServerCallback_GiveMatchLossProtection() +{ + clGlobal.showMatchLossProtection = true +} + +void function EnableDoDeathCallback( entity ent ) +{ + ent.DoDeathCallback( true ) +} + + + +int function UpdateSubText2ForRiffs( AnnouncementData announcement ) +{ + array riffTexts = [] + + if ( IsPilotEliminationBased() ) + riffTexts.append( "#GAMESTATE_NO_RESPAWNING" ) + + if ( Riff_FloorIsLava() ) + riffTexts.append( "#GAMEMODE_FLOOR_IS_LAVA_SUBTEXT2" ) + + if ( level.nv.minimapState == eMinimapState.Hidden ) + riffTexts.append( "#GAMESTATE_NO_MINIMAP" ) + + if ( level.nv.ammoLimit == eAmmoLimit.Limited ) + riffTexts.append( "#GAMESTATE_LIMITED_AMMUNITION" ) + + if ( level.nv.titanAvailability != eTitanAvailability.Default ) + { + switch ( level.nv.titanAvailability ) + { + case eTitanAvailability.Always: + riffTexts.append( "#GAMESTATE_UNLIMITED_TITANS" ) + break + case eTitanAvailability.Once: + riffTexts.append( "#GAMESTATE_ONE_TITAN" ) + break + case eTitanAvailability.Never: + riffTexts.append( "#GAMESTATE_NO_TITANS" ) + break + } + } + + if ( level.nv.allowNPCs != eAllowNPCs.Default ) + { + switch ( level.nv.allowNPCs ) + { + case eAllowNPCs.None: + //riffTexts.append( "#GAMESTATE_NO_MINIONS" ) + break + + case eAllowNPCs.GruntOnly: + riffTexts.append( "#GAMESTATE_GRUNTS_ONLY" ) + break + + case eAllowNPCs.SpectreOnly: + riffTexts.append( "#GAMESTATE_SPECTRES_ONLY" ) + break + } + } + + float pilotHealthMultiplier = GetCurrentPlaylistVarFloat( "pilot_health_multiplier", 0.0 ) + if ( pilotHealthMultiplier != 0.0 && pilotHealthMultiplier <= 1.5 ) + riffTexts.append( "#GAMESTATE_LOW_PILOT_HEALTH" ) + else if ( pilotHealthMultiplier > 1.5 ) + riffTexts.append( "#GAMESTATE_HIGH_PILOT_HEALTH" ) + + switch ( riffTexts.len() ) + { + case 1: + Announcement_SetSubText2( announcement, riffTexts[0] ) + break + case 2: + Announcement_SetSubText2( announcement, "#GAMEMODE_ANNOUNCEMENT_SUBTEXT_2", riffTexts[0], riffTexts[1] ) + break + case 3: + Announcement_SetSubText2( announcement, "#GAMEMODE_ANNOUNCEMENT_SUBTEXT_3", riffTexts[0], riffTexts[1], riffTexts[2] ) + break + case 4: + Announcement_SetSubText2( announcement, "#GAMEMODE_ANNOUNCEMENT_SUBTEXT_4", riffTexts[0], riffTexts[1], riffTexts[2], riffTexts[3] ) + break + case 5: + Announcement_SetSubText2( announcement, "#GAMEMODE_ANNOUNCEMENT_SUBTEXT_5", riffTexts[0], riffTexts[1], riffTexts[2], riffTexts[3], riffTexts[4] ) + break + + default: + Announcement_SetSubText2( announcement, "", "" ) + return 0 + } + + return riffTexts.len() +} + + +void function ServerCallback_GameModeAnnouncement() +{ + entity player = GetLocalClientPlayer() + string gameMode = GameRules_GetGameMode() + + if ( GameMode_GetCustomIntroAnnouncement( gameMode ) != null ) + { + void functionref(entity) func = GameMode_GetCustomIntroAnnouncement( gameMode ) + func(player) + return + } + + int team = player.GetTeam() + + local totalDuration = 0.0 + + AnnouncementData announcement + + if ( GetGameState() == eGameState.Epilogue ) + { + // never gets hit?? + announcement = Announcement_Create( "#GAMEMODE_EPILOGUE" ) + } + else + { + announcement = Announcement_Create( GAMETYPE_TEXT[gameMode] ) + announcement.announcementStyle = ANNOUNCEMENT_STYLE_BIG + + Announcement_SetIcon( announcement, GAMETYPE_ICON[gameMode] ) + Announcement_SetSubText( announcement, GAMEDESC_CURRENT ) + + if ( GameMode_IsDefined( gameMode ) ) + { + if ( GameMode_GetAttackDesc( gameMode ) != "" && team == level.nv.attackingTeam ) + Announcement_SetSubText( announcement, GameMode_GetAttackDesc( gameMode ) ) + + if ( GameMode_GetDefendDesc( gameMode ) != "" && team != level.nv.attackingTeam ) + Announcement_SetSubText( announcement, GameMode_GetDefendDesc( gameMode ) ) + } + } + + int numRiffs = UpdateSubText2ForRiffs( announcement ) + float announcementDuration = numRiffs + DEFAULT_GAMEMODE_ANNOUNCEMENT_DURATION + if ( gameMode == COLISEUM ) + announcementDuration = 2.3 //JFS: Make coliseum announcement disappear with the black bars. Note that the rui fade out sequence time is a floor on how low announcementDuration can be set to + + Announcement_SetDuration( announcement, announcementDuration ) + totalDuration += announcementDuration + + AnnouncementFromClass( player, announcement ) // TODO: team specific goals + + if ( clGlobal.showMatchLossProtection ) + { + announcementDuration = 2.0 + totalDuration += announcementDuration + delaythread( announcementDuration ) DeathHintDisplay( "#LATE_JOIN_NO_LOSS" ) + } + else if ( clGlobal.canShowLateJoinMessage ) + { + if ( level.nv.matchProgress > 5 || GetRoundsPlayed() > 0 ) + { + announcementDuration = 2.0 + totalDuration += announcementDuration + delaythread( announcementDuration ) DeathHintDisplay( "#LATE_JOIN" ) + } + } + clGlobal.showMatchLossProtection = false + clGlobal.canShowLateJoinMessage = false + + if ( Riff_FloorIsLava() ) + { + announcementDuration = 10.0 + totalDuration += announcementDuration + //printt( "Total duration delayed for lava announcement: " + totalDuration ) + delaythread( totalDuration ) PlayConversationToLocalClient( "floor_is_lava_announcement" ) + } +} + + +function MainHud_InitScoreBars( vgui, entity player, scoreGroup ) +{ + local hudScores = {} + vgui.s.scoreboardProgressBars <- hudScores + + local panel = vgui.GetPanel() + + hudScores.GameInfoBG <- scoreGroup.CreateElement( "GameInfoBG", panel ) + + string gameMode = GameRules_GetGameMode() + int friendlyTeam = player.GetTeam() + + #if HAS_GAMEMODES + if ( IsFFAGame() ) + { + return + } + #endif + + int enemyTeam = friendlyTeam == TEAM_IMC ? TEAM_MILITIA : TEAM_IMC + + if ( IsRoundBased() ) + { + level.scoreLimit[TEAM_IMC] <- GetRoundScoreLimit_FromPlaylist() + level.scoreLimit[TEAM_MILITIA] <- GetRoundScoreLimit_FromPlaylist() + } + else + { + level.scoreLimit[TEAM_IMC] <- GetScoreLimit_FromPlaylist() + level.scoreLimit[TEAM_MILITIA] <- GetScoreLimit_FromPlaylist() + } + + #if HAS_GAMEMODES + Assert( gameMode == GameRules_GetGameMode() ) + switch ( gameMode ) + { + case CAPTURE_THE_FLAG: + vgui.s.friendlyFlag <- scoreGroup.CreateElement( "FriendlyFlag", panel ) + vgui.s.enemyFlag <- scoreGroup.CreateElement( "EnemyFlag", panel ) + + vgui.s.friendlyFlagLabel <- scoreGroup.CreateElement( "FriendlyFlagLabel", panel ) + vgui.s.enemyFlagLabel <- scoreGroup.CreateElement( "EnemyFlagLabel", panel ) + + thread CaptureTheFlagThink( vgui, player ) + break + + case MARKED_FOR_DEATH: + case MARKED_FOR_DEATH_PRO: + thread MarkedForDeathHudThink( vgui, player, scoreGroup ) + break + } + #endif + + thread TitanEliminationThink( vgui, player ) + + thread RoundScoreThink( vgui, scoreGroup, player ) + + vgui.s.scoreboardProgressGroup <- scoreGroup + + hudScores.GameInfoBG.Show() + + local scoreboardArrays = {} + vgui.s.scoreboardArrays <- scoreboardArrays + + //if ( ShouldUsePlayerStatusCount() ) //Can't just do PilotEliminationBased check here because it isn't set when first connecting + //{ + // //ToDo: Eventually turn it on for normal Titan count too. Need to make sure "Titan ready but not called in yet" icon doesn't get hidden by this element + // hudScores.Player_Status_BG <- scoreGroup.CreateElement( "Player_Status_BG", panel ) + // hudScores.Player_Status_BG.Show() + // + // CreatePlayerStatusElementsFriendly( scoreboardArrays, scoreGroup, panel ) + // CreatePlayerStatusElementsEnemy( scoreboardArrays, scoreGroup, panel ) + // + // thread ScoreBarsPlayerStatusThink( vgui, player, scoreboardArrays.FriendlyPlayerStatusCount, scoreboardArrays.EnemyPlayerStatusCount ) + //} + //else + //{ + // hudScores.Player_Status_BG <- scoreGroup.CreateElement( "Player_Status_BG", panel ) + // hudScores.Player_Status_BG.Show() + // thread ScoreBarsTitanCountThink( vgui, player, hudScores.FriendlyTitanCount, hudScores.FriendlyTitanReadyCount, hudScores.EnemyTitanCount ) + //} + + if ( IsWatchingReplay() ) + vgui.s.scoreboardProgressGroup.Hide() + + UpdatePlayerStatusCounts() + + if ( IsSuddenDeathGameMode() ) + thread SuddenDeathHUDThink( vgui, player ) +} + +function CaptureTheFlagThink( vgui, entity player ) +{ + vgui.EndSignal( "OnDestroy" ) + + if ( vgui instanceof C_VGuiScreen ) + player.EndSignal( "OnDestroy" ) + + vgui.s.friendlyFlag.Show() + vgui.s.enemyFlag.Show() + vgui.s.friendlyFlagLabel.Show() + vgui.s.enemyFlagLabel.Show() + + while ( GetGameState() < eGameState.Epilogue ) + { + if ( "friendlyFlagState" in player.s ) + { + switch ( player.s.friendlyFlagState ) + { + case eFlagState.None: + vgui.s.friendlyFlagLabel.SetText( "" ) + break + case eFlagState.Home: + vgui.s.friendlyFlagLabel.SetText( "#GAMEMODE_FLAG_HOME" ) + break + case eFlagState.Held: + vgui.s.friendlyFlagLabel.SetText( player.s.friendlyFlagCarrierName ) + break + case eFlagState.Away: + vgui.s.friendlyFlagLabel.SetText( "#GAMEMODE_FLAG_DROPPED" ) + break + } + + switch ( player.s.enemyFlagState ) + { + case eFlagState.None: + vgui.s.enemyFlagLabel.SetText( "" ) + break + case eFlagState.Home: + vgui.s.enemyFlagLabel.SetText( "#GAMEMODE_FLAG_HOME" ) + break + case eFlagState.Held: + vgui.s.enemyFlagLabel.SetText( player.s.enemyFlagCarrierName ) + break + case eFlagState.Away: + vgui.s.enemyFlagLabel.SetText( "#GAMEMODE_FLAG_DROPPED" ) + break + } + } + + clGlobal.levelEnt.WaitSignal( "FlagUpdate" ) + + WaitEndFrame() + } + + vgui.s.friendlyFlag.Hide() + vgui.s.enemyFlag.Hide() + vgui.s.friendlyFlagLabel.Hide() + vgui.s.enemyFlagLabel.Hide() +} + + + +function TitanEliminationThink( vgui, entity player ) +{ + vgui.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDestroy" ) + + if ( player != GetLocalClientPlayer() ) + return + + OnThreadEnd( + function() : ( player ) + { + if ( !IsValid( player ) ) + return + + if ( player.cv.hud.s.lastEventNotificationText == "#GAMEMODE_CALLINTITAN_COUNTDOWN" ) + HideEventNotification() + } + ) + + while ( true ) + { + if ( Riff_EliminationMode() == eEliminationMode.Titans ) + { + if ( IsAlive( player ) && GamePlayingOrSuddenDeath() && level.nv.secondsTitanCheckTime > Time() && !player.IsTitan() && !IsValid( player.GetPetTitan() ) && player.GetNextTitanRespawnAvailable() >= 0 ) + { + SetTimedEventNotificationHATT( level.nv.secondsTitanCheckTime - Time(), "#GAMEMODE_CALLINTITAN_COUNTDOWN", HATT_GAME_COUNTDOWN_SECONDS_MILLISECONDS, level.nv.secondsTitanCheckTime ) + } + else if ( player.cv.hud.s.lastEventNotificationText == "#GAMEMODE_CALLINTITAN_COUNTDOWN" ) + { + HideEventNotification() + } + } + else if ( Riff_EliminationMode() == eEliminationMode.Pilots ) + { + + } + + WaitSignal( player, "UpdateLastTitanStanding", "PetTitanChanged", "OnDeath" ) + } +} + +function RoundScoreThink( var vgui, var scoreGroup, entity player ) +{ + vgui.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDestroy" ) + + FlagWait( "EntitiesDidLoad" ) //Have to do this because the nv that determines if RoundBased or not might not get set yet + + int friendlyTeam = player.GetTeam() + int enemyTeam = friendlyTeam == TEAM_IMC ? TEAM_MILITIA : TEAM_IMC + + local isRoundBased = IsRoundBased() + bool showRoundScore = true + int roundScoreLimit = GetRoundScoreLimit_FromPlaylist() + int scoreLimit = GetScoreLimit_FromPlaylist() + + if ( isRoundBased && showRoundScore ) + { + level.scoreLimit[TEAM_IMC] <- roundScoreLimit + level.scoreLimit[TEAM_MILITIA] <- roundScoreLimit + } + else + { + level.scoreLimit[TEAM_IMC] <- scoreLimit + level.scoreLimit[TEAM_MILITIA] <- scoreLimit + } + + local hudScores = vgui.s.scoreboardProgressBars + + while ( true ) + { + if ( isRoundBased && showRoundScore ) + { + hudScores.Friendly_Number.SetAutoText( "", HATT_FRIENDLY_TEAM_ROUND_SCORE, 0 ) + hudScores.Enemy_Number.SetAutoText( "", HATT_ENEMY_TEAM_ROUND_SCORE, 0 ) + } + + hudScores.ScoresFriendly.SetBarProgressRemap( 0, level.scoreLimit[friendlyTeam], 0.011, 0.96 ) + hudScores.ScoresEnemy.SetBarProgressRemap( 0, level.scoreLimit[enemyTeam], 0.011, 0.96 ) + wait 1.0 + } +} + +function CreatePlayerStatusElementsFriendly( scoreboardArrays, scoreGroup, panel ) +{ + scoreboardArrays.FriendlyPlayerStatusCount <- arrayofsize( 8 ) + + for ( int i = 0; i < 8; ++i ) + { + scoreboardArrays.FriendlyPlayerStatusCount[ i ] = scoreGroup.CreateElement( "Friendly_Player_Status_" + i, panel ) + } +} + +function CreatePlayerStatusElementsEnemy( scoreboardArrays, scoreGroup, panel ) +{ + scoreboardArrays.EnemyPlayerStatusCount <- arrayofsize( 8 ) + + for ( int i = 0; i < 8; ++i ) + { + scoreboardArrays.EnemyPlayerStatusCount[ i ] = scoreGroup.CreateElement( "Enemy_Player_Status_" + i, panel ) + } +} + + +function ScoreBarsPlayerStatusThink( vgui, entity player, friendlyPlayerStatusElem, enemyPlayerStatusElem ) +{ + int friendlyTeam = player.GetTeam() + int enemyTeam = friendlyTeam == TEAM_IMC ? TEAM_MILITIA : TEAM_IMC + + vgui.EndSignal( "OnDestroy" ) + + while( true ) + { + clGlobal.levelEnt.WaitSignal( "UpdatePlayerStatusCounts" ) + + if ( IsWatchingReplay() ) //Don't update visibility if the scoreboardgroup should be hidden + continue + + UpdatePlayerStatusForTeam( friendlyTeam, friendlyPlayerStatusElem, $"ui/icon_status_titan_friendly", $"ui/icon_status_pilot_friendly", $"ui/icon_status_burncard_friendly", $"ui/icon_status_burncard_friendly" ) + UpdatePlayerStatusForTeam( enemyTeam, enemyPlayerStatusElem, $"ui/icon_status_titan_enemy", $"ui/icon_status_pilot_enemy", $"ui/icon_status_burncard_enemy", $"ui/icon_status_burncard_enemy" ) + } +} + +function CountPlayerStatusTypes( array teamPlayers ) +{ + table resultTable = { + titanWithBurnCard = 0, + titan = 0, + pilotWithBurnCard = 0 + pilot = 0, + } + + foreach ( player in teamPlayers ) + { + entity playerPetTitan = player.GetPetTitan() + + if ( !IsAlive( player ) ) + { + if ( IsAlive( playerPetTitan ) ) + resultTable.titan++ + } + else + { + if ( player.IsTitan() ) + resultTable.titan++ + else if ( IsAlive( playerPetTitan ) ) + resultTable.titan++ + else + resultTable.pilot++ + } + + } + + return resultTable +} + + +function UpdatePlayerStatusForTeam( int team, teamStatusElem, titanImage, pilotImage, pilotBurnCardImage, titanBurnCardImage ) +{ + array teamPlayers = GetPlayerArrayOfTeam( team ) + local teamResultTable = CountPlayerStatusTypes( teamPlayers ) + + int maxElems = 8 + + int index = 0 + int currentElem = 0 + + for ( index = 0; index < teamResultTable.titanWithBurnCard && currentElem < maxElems; index++, currentElem++ ) + { + teamStatusElem[ currentElem ].Show() + teamStatusElem[ currentElem ].SetImage( titanBurnCardImage ) + } + + for ( index = 0; index < teamResultTable.titan && index < maxElems; index++, currentElem++ ) + { + teamStatusElem[ currentElem ].Show() + teamStatusElem[ currentElem ].SetImage( titanImage ) + } + + for ( index = 0; index < teamResultTable.pilotWithBurnCard && index < maxElems; index++, currentElem++ ) + { + teamStatusElem[ currentElem ].Show() + teamStatusElem[ currentElem ].SetImage( pilotBurnCardImage ) + } + + for ( index = 0; index < teamResultTable.pilot && index < maxElems; index++, currentElem++ ) + { + teamStatusElem[ currentElem ].Show() + teamStatusElem[ currentElem ].SetImage( pilotImage ) + } + + for( ; currentElem < maxElems; currentElem++ ) + { + teamStatusElem[ currentElem ].Hide() + } +} +function SuddenDeathHUDThink( vgui, entity player ) +{ + Signal( player, "SuddenDeathHUDThink" ) + player.EndSignal( "SuddenDeathHUDThink" ) + vgui.EndSignal( "OnDestroy" ) + + while ( GetGameState() != eGameState.SuddenDeath ) + WaitSignal( player, "GameStateChanged" ) + + EndSignal( player, "GameStateChanged" ) + + local hudScores = vgui.s.scoreboardProgressBars + + OnThreadEnd( + function() : ( hudScores, player ) + { + if ( !IsValid( hudScores ) ) + return + + hudScores.GameInfo_Label.SetColor( 255, 255, 255, 255 ) + + string restoredGameModeLabelText = GAMETYPE_TEXT[ GameRules_GetGameMode() ] + hudScores.GameModeLabel.SetText( restoredGameModeLabelText ) + + if ( player == GetLocalClientPlayer() ) + { + local scoreElemsClient = player.cv.clientHud.s.mainVGUI.s.scoreboardProgressGroup.elements + scoreElemsClient.GameModeLabel.SetText( restoredGameModeLabelText ) + } + } + ) + + string gameModeLabelText = "" + + switch ( GAMETYPE ) + { + case CAPTURE_THE_FLAG: + gameModeLabelText = "#GAMEMODE_CAPTURE_THE_FLAG_SUDDEN_DEATH" + break + + case TEAM_DEATHMATCH: + case HARDCORE_TDM: + gameModeLabelText = "#GAMEMODE_PILOT_HUNTER_SUDDEN_DEATH" + break + + default: + gameModeLabelText = GAMETYPE_TEXT[ GameRules_GetGameMode() ] + } + + hudScores.GameModeLabel.SetText( gameModeLabelText ) + + if ( player == GetLocalClientPlayer() ) + { + local scoreElemsClient = player.cv.clientHud.s.mainVGUI.s.scoreboardProgressGroup.elements + scoreElemsClient.GameModeLabel.SetText( gameModeLabelText ) + } + + float startTime = Time() + float pulseFrac = 0.0 + + while ( true ) + { + pulseFrac = Graph( GetPulseFrac( 1.0, startTime ), 0.0, 1.0, 0.05, 1.0 ) + hudScores.GameInfo_Label.SetColor( 255, 255, 255, 255 * pulseFrac ) + + wait( 0.0 ) + } +} diff --git a/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut b/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut new file mode 100644 index 000000000..deccbac28 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/client/cl_screenfade.gnut @@ -0,0 +1,315 @@ +global function ClScreenfade_Init + +global function RegisterDoomProtectionHintDamage + +global function UpdateScreenFade + + +struct +{ + var screenFade = null + int lastAlpha = -1 + void functionref() screenFadeFunc +} file + +void function ClScreenfade_Init() +{ + RegisterSignal( "NewScreenFade" ) + if ( IsSingleplayer() ) + file.screenFadeFunc = UpdateScreenFade_SpFirstFrame + else + file.screenFadeFunc = UpdateScreenFadeInternal + + + thread PlayerPainSoundThread() + + AddCallback_OnClientScriptInit( ScreenFade_AddClient ) + + file.screenFade = RuiCreate( $"ui/screen_fade.rpak", clGlobal.topoFullScreen, RUI_DRAW_HUD, RUI_SORT_SCREENFADE ) + + if ( IsLobby() ) + return + + RuiSetFloat3( file.screenFade, "fadeColor", <0, 0, 0> ) + RuiSetFloat( file.screenFade, "fadeAlpha", 1.0 ) +} + +void function ScreenFade_AddClient( entity player ) +{ +} + + +void function UpdateScreenFade() +{ + file.screenFadeFunc() +} + +void function UpdateScreenFadeInternal() +{ + table fadeParams = expect table( GetFadeParams() ) + + //For debugging screen fade + /*int alpha = expect int (fadeParams.a ) + + if ( file.lastAlpha != alpha ) + printt( "Alpha changed in UpdateScreenFade to: " + alpha ) + + file.lastAlpha = alpha*/ + + RuiSetFloat3( file.screenFade, "fadeColor", < expect int( fadeParams.r ) / 255.0, expect int( fadeParams.g ) / 255.0, expect int( fadeParams.b ) / 255.0 > ) + RuiSetFloat( file.screenFade, "fadeAlpha", expect int( fadeParams.a ) / 255.0 ) +} + +void function UpdateScreenFade_SpFirstFrame() +{ + RuiSetFloat3( file.screenFade, "fadeColor", < 0, 0, 0 > ) + RuiSetFloat( file.screenFade, "fadeAlpha", 255 ) + file.screenFadeFunc = UpdateScreenFadeInternal +} + +float g_doomProtectionHintDamage = 0.0 +float g_doomProtectionHintLastShowedTime = 0.0 + +void function RegisterDoomProtectionHintDamage( float newAmount ) +{ + const float LOCKOUT_TIME = 20.0 + if ( newAmount < 0.0 ) + return + if ( (Time() - g_doomProtectionHintLastShowedTime) < LOCKOUT_TIME ) + return + + g_doomProtectionHintDamage += newAmount; + printt( "g_doomProtectionHintDamage is now:", g_doomProtectionHintDamage ) +} + +void function DoomProtectionHintThread() +{ + const float HINT_DURATION = 4.0 + const float THRESHOLD_PILOT = 1000 + const float THRESHOLD_TITAN = 2000 + const float FIRSTTIME_SCALE = 0.6 + + while ( true ) + { + WaitFrame() + + entity player = GetLocalViewPlayer() + if ( !IsValid( player ) ) + continue; + + float threshold = player.IsTitan() ? THRESHOLD_TITAN : THRESHOLD_PILOT + if ( g_doomProtectionHintLastShowedTime == 0.0 ) + threshold *= FIRSTTIME_SCALE + + if ( g_doomProtectionHintDamage > threshold ) + { + wait 0.4 + entity player = GetLocalViewPlayer() + if ( IsValid( player ) ) + { + SetTimedEventNotification( HINT_DURATION, player.IsTitan() ? "#NOTIFY_HINT_TITAN_USE_FINISHERS" : "#NOTIFY_HINT_PILOTS_USE_FINISHERS" ) + g_doomProtectionHintLastShowedTime = Time() + g_doomProtectionHintDamage = 0.0 + } + } + } +} + +string function GetPainSound( entity player, string varName ) +{ + var resultRaw = player.GetPlayerSettingsField( varName ) + if ( resultRaw == null ) + { + Assert( 0, ("Invalid player setting field: " + varName) ) + return "" + } + + return expect string( resultRaw ) +} + +void function PlayerPainSoundThread() +{ + // Each layer has: + //: begin threshold (health falls below XX) + //: end threshold (health has risen back up above YY) + //: looping sound + //: endcap sound + + float HEALTH_PERCENT_LAYER1 = 0.85; + float HEALTH_PERCENT_LAYER1_END = 0.85; + float HEALTH_PERCENT_LAYER2 = 0.55; + float HEALTH_PERCENT_LAYER2_END = 0.55; + float HEALTH_PERCENT_LAYER3 = 0.55; + float HEALTH_PERCENT_LAYER3_END = 0.59; + + if ( shGlobal.proto_pilotHealthRegenDisabled ) + { + HEALTH_PERCENT_LAYER1 *= 0.33 + HEALTH_PERCENT_LAYER1_END *= 0.33 + HEALTH_PERCENT_LAYER2 *= 0.33 + HEALTH_PERCENT_LAYER2_END *= 0.33 + HEALTH_PERCENT_LAYER3 *= 0.33 + HEALTH_PERCENT_LAYER3_END *= 0.33 + } + + entity ourPlayer = null; + bool arePlayingLayer1 = false; + bool arePlayingLayer2 = false; + bool arePlayingLayer3 = false; + + string soundLayer1Loop = "" + string soundLayer1End = "" + string soundLayer2Start = "" + string soundLayer2Loop = "" + string soundLayer3Loop = "" + string soundLayer3End = "" + + while ( true ) + { + bool shouldPlayLayer1 = false + bool shouldPlayLayer2 = false + bool shouldPlayLayer3 = false + bool endcapsAllowed = false + entity localViewPlayer = GetLocalViewPlayer(); + + if ( !IsValid( localViewPlayer ) ) + { + } + else if ( !IsAlive( localViewPlayer ) ) + { + } + else if ( (ourPlayer != null) && (ourPlayer != localViewPlayer) ) + { + } + else if ( localViewPlayer.IsTitan() ) + { + endcapsAllowed = true + } + else + { + endcapsAllowed = true + + int health = localViewPlayer.GetHealth() + int maxHealth = localViewPlayer.GetMaxHealth() + float healthPercent = ((maxHealth > 0) ? (health.tofloat() / maxHealth.tofloat()) : 1.0) + + if ( !arePlayingLayer1 && (healthPercent <= HEALTH_PERCENT_LAYER1) ) + shouldPlayLayer1 = true + else if ( arePlayingLayer1 && (healthPercent <= HEALTH_PERCENT_LAYER1_END) ) + shouldPlayLayer1 = true + + if ( !arePlayingLayer2 && (healthPercent <= HEALTH_PERCENT_LAYER2) ) + shouldPlayLayer2 = true + else if ( arePlayingLayer2 && (healthPercent <= HEALTH_PERCENT_LAYER2_END) ) + shouldPlayLayer2 = true + + if ( !arePlayingLayer3 && (healthPercent <= HEALTH_PERCENT_LAYER3) ) + shouldPlayLayer3 = true + else if ( arePlayingLayer3 && (healthPercent <= HEALTH_PERCENT_LAYER3_END) ) + shouldPlayLayer3 = true + } + + if ( shouldPlayLayer1 != arePlayingLayer1 ) + { + if ( shouldPlayLayer1 ) + { + //printt( "LAYER 1 STARTS" ) + arePlayingLayer1 = true + Assert( (ourPlayer == null) || (ourPlayer == localViewPlayer) ) + ourPlayer = localViewPlayer + + soundLayer1Loop = GetPainSound( ourPlayer, "sound_pain_layer1_loop" ) + soundLayer1End = GetPainSound( ourPlayer, "sound_pain_layer1_end" ) + if ( soundLayer1Loop != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer1Loop ) + } + else + { + //printt( "LAYER 1 _stop_" ) + if ( IsValid( ourPlayer ) ) + { + if ( soundLayer1Loop != "" ) + StopSoundOnEntity( ourPlayer, soundLayer1Loop ) + if ( endcapsAllowed && (soundLayer1End != "") ) + EmitSoundOnEntity( ourPlayer, soundLayer1End ) + } + arePlayingLayer1 = false; + } + } + + if ( shouldPlayLayer2 != arePlayingLayer2 ) + { + if ( shouldPlayLayer2 ) + { + //printt( "LAYER 2 STARTS" ); + arePlayingLayer2 = true; + Assert( (ourPlayer == null) || (ourPlayer == localViewPlayer) ) + ourPlayer = localViewPlayer; + soundLayer2Start = GetPainSound( ourPlayer, "sound_pain_layer2_start" ) + soundLayer2Loop = GetPainSound( ourPlayer, "sound_pain_layer2_loop" ) + if ( soundLayer2Start != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer2Start ) + if ( soundLayer2Loop != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer2Loop ) + } + else + { + //printt( "LAYER 2 _stop_" ); + if ( IsValid( ourPlayer ) ) + { + if ( soundLayer2Start != "" ) + StopSoundOnEntity( ourPlayer, soundLayer2Start ) + if ( soundLayer2Loop != "" ) + StopSoundOnEntity( ourPlayer, soundLayer2Loop ) + } + arePlayingLayer2 = false; + } + } + + if ( shouldPlayLayer3 != arePlayingLayer3 ) + { + if ( shouldPlayLayer3 ) + { + //printt( "LAYER 3 STARTS" ) + arePlayingLayer3 = true + Assert( (ourPlayer == null) || (ourPlayer == localViewPlayer) ) + ourPlayer = localViewPlayer + soundLayer3Loop = GetPainSound( ourPlayer, "sound_pain_layer3_loop" ) + soundLayer3End = GetPainSound( ourPlayer, "sound_pain_layer3_end" ) + if ( soundLayer3Loop != "" ) + EmitSoundOnEntity( ourPlayer, soundLayer3Loop ) + } + else + { + //printt( "LAYER 3 _stop_" ) + if ( IsValid( ourPlayer ) ) + { + if ( soundLayer3Loop != "" ) + StopSoundOnEntity( ourPlayer, soundLayer3Loop ) + if ( endcapsAllowed && (soundLayer3End != "") ) + EmitSoundOnEntity( ourPlayer, soundLayer3End ) + } + arePlayingLayer3 = false; + } + } + + if ( !arePlayingLayer1 && !arePlayingLayer2 && !arePlayingLayer3 ) + ourPlayer = null + + WaitFrame() + } +} + + +/* +void function ClientSetPilotPainFlashColor( entity player, int a ) +{ + player.hudElems.damageOverlayPainFlash.SetColor( 255, 255, 255, a ) + + if ( a > 0 ) + player.hudElems.damageOverlayPainFlash.Show() + else + player.hudElems.damageOverlayPainFlash.Hide() +} +*/ + diff --git a/Northstar.Client/mod/scripts/vscripts/mp/levels/cl_mp_glitch.nut b/Northstar.Client/mod/scripts/vscripts/mp/levels/cl_mp_glitch.nut new file mode 100644 index 000000000..4ca323919 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/mp/levels/cl_mp_glitch.nut @@ -0,0 +1,171 @@ +global function ClientCodeCallback_MapInit +global function AddInWorldMinimapObject + +// someday, move this to in world minimap + +struct +{ + array minimapBGTopos + array minimapTopos + array screens + float mapCornerX + float mapCornerY + float mapScale + float threatMaxDist +} file + +void function ClientCodeCallback_MapInit() +{ + AddCallback_EntitiesDidLoad( EntitiesDidLoad ) + AddCallback_MinimapEntSpawned( AddInWorldMinimapObject ) + AddCallback_LocalViewPlayerSpawned( AddInWorldMinimapObject ) +} + +void function EntitiesDidLoad() +{ + InitMinimapScreens() +} + +var function AddInWorldMinimapTopo( entity ent, float width, float height ) +{ + vector ang = ent.GetAngles() + vector right = ( (AnglesToRight( ang )*-1) * width * 0.5 ) + vector down = ( (AnglesToUp( ang )*-1) * height * 0.5 ) + + vector org = ent.GetOrigin() + + org = ent.GetOrigin() - right*0.5 - down*0.5 + + var topo = RuiTopology_CreatePlane( org, right, down, true ) + return topo +} + +void function InitMinimapScreens() +{ + array screens = GetEntArrayByScriptName( "inworld_minimap" ) + foreach ( screen in screens ) + { + file.minimapTopos.append( AddInWorldMinimapTopo( screen, 350, 350 ) ) + file.minimapBGTopos.append( AddInWorldMinimapTopo( screen, 450, 450 ) ) + } + + asset mapImage = Minimap_GetAssetForKey( "minimap" ) + file.mapCornerX = Minimap_GetFloatForKey( "pos_x" ) + file.mapCornerY = Minimap_GetFloatForKey( "pos_y" ) + float displayDist = Minimap_GetFloatForKey( "displayDist" ) + float threatDistNear = Minimap_GetFloatForKey( "threatNearDist" ) + float threatDistFar = Minimap_GetFloatForKey( "threatFarDist" ) + file.mapScale = Minimap_GetFloatForKey( "scale" ) + + file.threatMaxDist = Minimap_GetFloatForKey( "threatMaxDist" ) + + foreach ( screen in file.minimapBGTopos ) + { + entity player = GetLocalViewPlayer() + var rui = RuiCreate( $"ui/in_world_minimap_border.rpak", screen, RUI_DRAW_WORLD, 0 ) + string factionChoice = GetFactionChoice( player ) + ItemDisplayData displayData = GetItemDisplayData( factionChoice ) + asset factionLogo = displayData.image + RuiSetImage( rui, "logo", factionLogo ) + RuiSetImage( rui, "basicImage", $"overviews/mp_glitch_wallmap_bracket" ) + } + foreach ( screen in file.minimapTopos ) + { + var rui = RuiCreate( $"ui/in_world_minimap_base.rpak", screen, RUI_DRAW_WORLD, 0 ) + RuiSetImage( rui, "mapImage", $"overviews/mp_glitch_wallmap" ) + RuiSetFloat3( rui, "mapCorner", ) + RuiSetFloat( rui, "displayDist", max( file.threatMaxDist, 2200 ) ) + RuiSetFloat( rui, "mapScale", file.mapScale ) + file.screens.append( rui ) + } + + foreach ( player in GetPlayerArray() ) + { + if ( IsValid( player ) ) + AddInWorldMinimapObject( player ) + } +} + +void function AddInWorldMinimapObject( entity ent ) //TODO: If we want radar jammer boost to hide friendly players we need to be able to get the rui handles back. +{ + Assert( IsValid( ent ) ) + + if ( !ent.IsPlayer() && !ent.IsTitan() ) + return + + ent.SetDoDestroyCallback( true ) + + foreach ( screen in file.minimapTopos ) + thread AddInWorldMinimapObjectInternal( ent, screen ) +} + +void function AddInWorldMinimapObjectInternal( entity ent, var screen ) +{ + bool isNPCTitan = ent.IsNPC() && ent.IsTitan() + bool isPetTitan = ent == GetLocalViewPlayer().GetPetTitan() + bool isLocalPlayer = ent == GetLocalViewPlayer() + int customState = ent.Minimap_GetCustomState() + asset minimapAsset = $"ui/in_world_minimap_player.rpak" + if ( isNPCTitan ) + { + minimapAsset = $"ui/in_world_minimap_object.rpak" + } + + int zOrder = ent.Minimap_GetZOrder() + entity viewPlayer = GetLocalViewPlayer() + + var rui = RuiCreate( minimapAsset, screen, RUI_DRAW_WORLD, MINIMAP_Z_BASE + zOrder ) + + //RuiTrackGameTime( rui, "lastFireTime", ent, RUI_TRACK_LAST_FIRED_TIME ) + + RuiSetFloat3( rui, "mapCorner", ) + RuiSetFloat( rui, "mapScale", file.mapScale ) + + RuiTrackFloat3( rui, "objectPos", ent, RUI_TRACK_ABSORIGIN_FOLLOW ) + RuiTrackFloat3( rui, "objectAngles", ent, RUI_TRACK_EYEANGLES_FOLLOW ) + RuiTrackInt( rui, "objectFlags", ent, RUI_TRACK_MINIMAP_FLAGS ) + RuiTrackInt( rui, "customState", ent, RUI_TRACK_MINIMAP_CUSTOM_STATE ) + RuiSetFloat( rui, "displayDist", max( file.threatMaxDist, 2200 ) ) + + if ( isLocalPlayer ) + RuiSetBool( rui, "isLocalPlayer", isLocalPlayer ) + + // MinimapPackage_PlayerInit( ent, rui ) + + if ( isPetTitan ) + { + RuiSetBool( rui, "useTeamColor", false ) + RuiSetFloat3( rui, "iconColor", TEAM_COLOR_YOU / 255.0 ) + } + + OnThreadEnd( + function() : ( rui ) + { + RuiDestroy( rui ) + } + ) + + ent.EndSignal( "OnDestroy" ) + + if ( ent.IsPlayer() ) + { + while ( IsValid( ent ) ) + { + WaitSignal( ent, "SettingsChanged", "OnDeath" ) + } + } + else + { + ent.WaitSignal( "OnDestroy" ) + } +} + +void function MinimapPackage_PlayerInit( entity ent, var rui ) +{ + RuiTrackGameTime( rui, "lastFireTime", ent, RUI_TRACK_LAST_FIRED_TIME ) + if ( !IsFFAGame() ) //JFS: Too much work to get FFA to work correctly with Minimap logic, so disabling it for FFA + { + RuiTrackFloat( rui, "sonarDetectedFrac", ent, RUI_TRACK_STATUS_EFFECT_SEVERITY, eStatusEffect.sonar_detected ) + RuiTrackFloat( rui, "maphackDetectedFrac", ent, RUI_TRACK_STATUS_EFFECT_SEVERITY, eStatusEffect.maphack_detected ) + } +} \ No newline at end of file diff --git a/Northstar.Client/mod/scripts/vscripts/presence/cl_presence.nut b/Northstar.Client/mod/scripts/vscripts/presence/cl_presence.nut index 142c94bad..191ef1444 100644 --- a/Northstar.Client/mod/scripts/vscripts/presence/cl_presence.nut +++ b/Northstar.Client/mod/scripts/vscripts/presence/cl_presence.nut @@ -22,14 +22,21 @@ GameStateStruct function DiscordRPC_GenerateGameState( GameStateStruct gs ) gs.mapDisplayname = Localize(GetMapDisplayName(GetMapName())) gs.playlist = GetCurrentPlaylistName() - gs.playlistDisplayname = Localize(GetCurrentPlaylistVarString("name", GetCurrentPlaylistName())) + gs.playlistDisplayname = Localize( GetCurrentPlaylistVarString( "name", GetCurrentPlaylistName() ) ) - gs.currentPlayers = GetPlayerArray().len() - gs.maxPlayers = GetCurrentPlaylistVarInt( "maxPlayers", -1 ) + int reservedCount = GetTotalPendingPlayersReserved() + int connectingCount = GetTotalPendingPlayersConnecting() + int loadingCount = GetTotalPendingPlayersLoading() + int connectedCount = GetPlayerArray().len() + int allKnownPlayersCount = reservedCount + connectingCount + loadingCount + connectedCount + + gs.currentPlayers = allKnownPlayersCount + gs.maxPlayers = GetCurrentPlaylistVarInt( "max_players", 16 ) if ( IsValid( GetLocalClientPlayer() ) ) gs.ownScore = GameRules_GetTeamScore( GetLocalClientPlayer().GetTeam() ) + #if MP if ( GameRules_GetGameMode() == FD ) { gs.playlist = "fd" // So it returns only one thing to the plugin side instead of the 5 separate difficulties FD have @@ -41,6 +48,9 @@ GameStateStruct function DiscordRPC_GenerateGameState( GameStateStruct gs ) else gs.fd_waveNumber = -1 // Tells plugin it's on Wave Break } + #else + gs.fd_waveNumber = -1 // Unecessary for campaign so return -1 + #endif gs.serverGameState = GetGameState() == -1 ? 0 : GetGameState() gs.otherHighestScore = gs.ownScore == highestScore ? secondHighest : highestScore diff --git a/Northstar.Client/mod/scripts/vscripts/ui/atlas_auth.nut b/Northstar.Client/mod/scripts/vscripts/ui/atlas_auth.nut index 89b7f7196..966686025 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/atlas_auth.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/atlas_auth.nut @@ -37,7 +37,7 @@ void function AtlasAuthDialog_Threaded() if ( res.errorCode != "" ) dialogData.message += format( "\n\n%s", Localize( "#AUTHENTICATION_FAILED_ERROR_CODE", res.errorCode ) ) - string link = "https://r2northstar.gitbook.io/r2northstar-wiki/installing-northstar/troubleshooting" + string link = "https://docs.northstar.tf/Wiki/installing-northstar/troubleshooting/" // link to generic troubleshooting page if we don't have an error code from Atlas if ( res.errorCode != "" ) link = format( "%s#%s", link, res.errorCode ) diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut index 35c9e9bae..ac617a9c7 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ingame.nut @@ -85,6 +85,9 @@ void function InitInGameMPMenu() var gameHeader = AddComboButtonHeader( comboStruct, headerIndex, "#MENU_HEADER_GAME" ) var leaveButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#LEAVE_MATCH" ) Hud_AddEventHandler( leaveButton, UIE_CLICK, OnLeaveButton_Activate ) + var teamChangeButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "#SWITCH_TEAMS" ) + Hud_AddEventHandler( teamChangeButton, UIE_CLICK, OnRequestTeamSwitch ) + thread UpdateTeamSwitchButton_Threaded( teamChangeButton ) #if DEV var devButton = AddComboButton( comboStruct, headerIndex, buttonIndex++, "Dev" ) Hud_AddEventHandler( devButton, UIE_CLICK, AdvanceMenuEventHandler( GetMenu( "DevMenu" ) ) ) @@ -700,3 +703,21 @@ void function SetTitanSelectButtonVisibleState( bool state ) Hud_Hide( file.titanSelectButton ) } } + +void function UpdateTeamSwitchButton_Threaded( var button ) +{ + while ( true ) + { + Hud_SetLocked( button, !GetConVarBool( "ns_allow_team_change" ) ) + wait 0.5 + } +} + +void function OnRequestTeamSwitch( var button ) +{ + if ( !Hud_IsLocked( button ) ) + { + ClientCommand( "changeteam" ) + CloseAllMenus() + } +} 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 3d1cc15f8..109eed129 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut @@ -1,83 +1,551 @@ +untyped global function InitModesMenu +global function NSSetModeCategory + +global enum eModeMenuModeCategory +{ + UNKNOWN = 0, + PVPVE = 1, + PVE = 2, + PVP = 3, + FFA = 4, + TITAN = 5, + OTHER = 6, + CUSTOM = 7 + + SIZE +} + +// List of blocked modes due to them being unfinished +const array blockedModes = +[ + "fd_easy", + "fd_normal", + "fd_hard", + "fd_master", + "fd_insane" +] + +struct ListEntry_t { + string mode + int category +} + +// Slider mouse delta buffer +struct { + int deltaX = 0 + int deltaY = 0 +} mouseDeltaBuffer struct { - int currentModePage + int scrollOffset + var menu + + string searchString + int searchEnum + + // Table of category overrides + table categoryOverrides + + // List of all modes we know + array modes + + // Sorted list of modes we want to show with categories included + array sortedModes } file const int MODES_PER_PAGE = 15 void function InitModesMenu() { - var menu = GetMenu( "ModesMenu" ) + file.menu = GetMenu( "ModesMenu" ) + + AddMouseMovementCaptureHandler( Hud_GetChild( file.menu, "MouseMovementCapture"), UpdateMouseDeltaBuffer ) + + AddMenuEventHandler( file.menu, eUIEvent.MENU_CLOSE, OnCloseModesMenu ) + AddMenuEventHandler( file.menu, eUIEvent.MENU_OPEN, OnOpenModesMenu ) + AddButtonEventHandler( Hud_GetChild( file.menu, "BtnModeListUpArrow"), UIE_CLICK, OnUpArrowSelected ) + AddButtonEventHandler( Hud_GetChild( file.menu, "BtnModeListDownArrow"), UIE_CLICK, OnDownArrowSelected ) - AddMenuEventHandler( menu, eUIEvent.MENU_OPEN, OnOpenModesMenu ) + AddButtonEventHandler( Hud_GetChild( file.menu, "BtnModeLabel"), UIE_CHANGE, FilterAndUpdateList ) + AddButtonEventHandler( Hud_GetChild( file.menu, "BtnModeSearch"), UIE_CHANGE, FilterAndUpdateList ) + AddButtonEventHandler( Hud_GetChild( file.menu, "SwtModeLabel"), UIE_CHANGE, FilterAndUpdateList ) - AddEventHandlerToButtonClass( menu, "ModeButton", UIE_GET_FOCUS, ModeButton_GetFocus ) - AddEventHandlerToButtonClass( menu, "ModeButton", UIE_CLICK, ModeButton_Click ) + AddButtonEventHandler( Hud_GetChild( file.menu, "BtnModeFiltersClear"), UIE_CLICK, OnBtnFiltersClear_Activate ) + + array buttons = GetElementsByClassname( file.menu, "ModeSelectorPanel" ) + foreach ( var panel in buttons ) + { + AddEventHandlerToButton( panel, "BtnMode", UIE_GET_FOCUS, ModeButton_GetFocus ) + AddEventHandlerToButton( panel, "BtnMode", UIE_CLICK, ModeButton_Click ) + } - AddMenuFooterOption( menu, BUTTON_A, "#A_BUTTON_SELECT" ) - AddMenuFooterOption( menu, BUTTON_B, "#B_BUTTON_BACK", "#BACK" ) - - AddMenuFooterOption( menu, BUTTON_SHOULDER_LEFT, "#PRIVATE_MATCH_PAGE_PREV", "#PRIVATE_MATCH_PAGE_PREV", CycleModesBack ) - AddMenuFooterOption( menu, BUTTON_SHOULDER_RIGHT, "#PRIVATE_MATCH_PAGE_NEXT", "#PRIVATE_MATCH_PAGE_NEXT", CycleModesForward ) + Hud_SetText( Hud_GetChild( file.menu, "SwtModeLabel" ), "#MODE_MENU_SWITCH" ) + SetButtonRuiText( Hud_GetChild( file.menu, "SwtModeLabel" ), "" ) + Hud_DialogList_AddListItem( Hud_GetChild( file.menu, "SwtModeLabel" ) , "#MODE_MENU_ALL", "-1" ) + for( int i = 0; i < eModeMenuModeCategory.SIZE; i++ ) + { + Hud_DialogList_AddListItem( Hud_GetChild( file.menu, "SwtModeLabel" ) , GetCategoryStringFromEnum(i), string(i) ) + } + + AddMenuFooterOption( file.menu, BUTTON_A, "#A_BUTTON_SELECT" ) + AddMenuFooterOption( file.menu, BUTTON_B, "#B_BUTTON_BACK", "#BACK" ) +} + +void function NSSetModeCategory( string mode, int category ) +{ + if( mode in file.categoryOverrides ) + { + file.categoryOverrides[mode] = category + printt( "Overwriting category for mode:", mode ) + return + } + + file.categoryOverrides[mode] <- category +} + +void function OnBtnFiltersClear_Activate( var b ) +{ + file.searchString = "" + file.searchEnum = -1 + + SetConVarInt( "modemenu_mode_filter", -1 ) + Hud_SetText( Hud_GetChild( file.menu, "BtnModeSearch"), "" ) + + file.scrollOffset = 0 + + BuildSortedModesArray() + UpdateListSliderHeight(float(file.sortedModes.len())) + UpdateListSliderPosition(file.sortedModes.len()) + UpdateVisibleModes() +} + +void function FilterAndUpdateList( var n ) +{ + file.searchString = Hud_GetUTF8Text( Hud_GetChild( file.menu, "BtnModeSearch" ) ) + file.searchEnum = GetConVarInt( "modemenu_mode_filter" ) + + file.scrollOffset = 0 + + BuildSortedModesArray() + UpdateListSliderHeight(float(file.sortedModes.len())) + UpdateListSliderPosition(file.sortedModes.len()) + UpdateVisibleModes() } void function OnOpenModesMenu() { + RegisterButtonPressedCallback( MOUSE_WHEEL_UP , OnScrollUp ) + RegisterButtonPressedCallback( MOUSE_WHEEL_DOWN , OnScrollDown ) + + // Reset filters + file.searchString = "" + file.searchEnum = -1 + + // We rebuild the modes array on open menu to make sure + // all modes get listed + BuildModesArray() + BuildSortedModesArray() + + UpdateListSliderHeight(float(file.sortedModes.len())) + UpdateListSliderPosition(file.sortedModes.len()) UpdateVisibleModes() - - if ( level.ui.privatematch_mode == 0 ) // set to the first mode if there's no mode focused - Hud_SetFocused( GetElementsByClassname( GetMenu( "ModesMenu" ), "ModeButton" )[ 0 ] ) + + // Set to the first mode if there's no mode focused + if ( level.ui.privatematch_mode == 0 ) + { + array panels = GetElementsByClassname( file.menu, "ModeSelectorPanel" ) + foreach( var panel in panels ) + { + if( Hud_IsEnabled( Hud_GetChild( panel, "BtnMode") ) ) + { + Hud_SetFocused( Hud_GetChild( panel, "BtnMode") ) + break + } + } + } +} + +void function OnCloseModesMenu() +{ + try + { + DeregisterButtonPressedCallback( MOUSE_WHEEL_UP , OnScrollUp ) + DeregisterButtonPressedCallback( MOUSE_WHEEL_DOWN , OnScrollDown ) + } + catch ( ex ) {} } +string function GetCategoryStringFromEnum( int category ) +{ + switch( category ) + { + case eModeMenuModeCategory.PVPVE: return "#MODE_MENU_PVPVE" + case eModeMenuModeCategory.PVE: return "#MODE_MENU_PVE" + case eModeMenuModeCategory.PVP: return "#MODE_MENU_PVP" + case eModeMenuModeCategory.FFA: return "#MODE_MENU_FFA" + case eModeMenuModeCategory.TITAN: return "#MODE_MENU_TITAN_ONLY" + case eModeMenuModeCategory.OTHER: return "#MODE_MENU_OTHER" + case eModeMenuModeCategory.CUSTOM: return "#MODE_MENU_CUSTOM" + } + + return "#MODE_MENU_UNKNOWN" +} + +void function BuildModesArray() +{ + file.modes.clear() + + foreach( string mode in GetPrivateMatchModes() ) + { + ListEntry_t entry + entry.mode = mode + entry.category = eModeMenuModeCategory.UNKNOWN + + switch( mode ) + { + case "aitdm": + case "at": + entry.category = eModeMenuModeCategory.PVPVE + break + case "fd_easy": + case "fd_normal": + case "fd_hard": + case "fd_master": + case "fd_insane": + entry.category = eModeMenuModeCategory.PVE + break + case "tdm": + case "ctf": + case "mfd": + case "ps": + case "cp": + case "speedball": + case "rocket_lf": + case "holopilot_lf": + entry.category = eModeMenuModeCategory.PVP + break + case "ffa": + case "fra": + entry.category = eModeMenuModeCategory.FFA + break + case "lts": + case "ttdm": + case "attdm": + case "turbo_ttdm": + case "alts": + case "turbo_lts": + entry.category = eModeMenuModeCategory.TITAN + break + case "coliseum": + case "sp_coop": + entry.category = eModeMenuModeCategory.OTHER + break + case "chamber": + case "hidden": + case "sns": + case "fw": + case "gg": + case "tt": + case "inf": + case "kr": + case "fastball": + case "hs": + case "ctf_comp": + case "tffa": + entry.category = eModeMenuModeCategory.CUSTOM + break + } + + file.modes.append(entry) + } +} + +int function SortModesAlphabetize( string a, string b ) +{ + a = Localize( GetGameModeDisplayName( a ) ) + b = Localize( GetGameModeDisplayName( b ) ) + + if ( a > b ) + return 1 + + if ( a < b ) + return -1 + + return 0 +} + +void function BuildSortedModesArray() +{ + file.sortedModes.clear() + + // Build sorted list of categories + array categories + for( int i = 0; i < eModeMenuModeCategory.SIZE; i++ ) + { + if( file.searchEnum != -1 && file.searchEnum != i ) + continue + + categories.append( GetCategoryStringFromEnum( i ) ) + } + + categories.sort( SortStringAlphabetize ) + + // Build final list of mixed modes and categories + foreach( string category in categories ) + { + // Build sorted list of modes in category + array modes + foreach( ListEntry_t entry in file.modes ) + { + int iCategory = entry.category + if( entry.mode in file.categoryOverrides ) + iCategory = file.categoryOverrides[entry.mode] + + if( GetCategoryStringFromEnum( iCategory ) != category ) + continue + + string mode = entry.mode + + if( file.searchString != "" && Localize(GetGameModeDisplayName(mode)).tolower().find(file.searchString.tolower()) == null ) + continue + + if( !modes.contains(mode) ) + modes.append( mode ) + } + + modes.sort( SortModesAlphabetize ) + + if( modes.len() == 0 ) + continue + + // Add to final list we then display + file.sortedModes.append( category ) + foreach( string mode in modes ) + file.sortedModes.append( mode ) + } +} + +//////////////////////////// +// 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.sortedModes.len() < MODES_PER_PAGE ) + return + + var sliderButton = Hud_GetChild( file.menu , "BtnModeListSlider" ) + var sliderPanel = Hud_GetChild( file.menu , "BtnModeListSliderPanel" ) + var movementCapture = Hud_GetChild( file.menu , "MouseMovementCapture" ) + + Hud_SetFocused( sliderButton ) + + int[2] screenSize = GetScreenSize() + float minYPos = -40.0 * ( screenSize[1] / 1080.0 ) + float maxHeight = 596.0 * ( screenSize[1] / 1080.0 ) + float maxYPos = minYPos - ( maxHeight - Hud_GetHeight( sliderPanel ) ) + float useableSpace = maxHeight - Hud_GetHeight( sliderPanel ) + + float jump = minYPos - ( useableSpace / ( float( file.sortedModes.len() ) ) ) + + // got local from official respaw scripts, without untyped throws an error + local pos = Hud_GetPos( sliderButton )[1] + local newPos = 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.sortedModes.len() - MODES_PER_PAGE ) ) + UpdateVisibleModes() +} + +void function UpdateListSliderHeight( float modes ) +{ + var sliderButton = Hud_GetChild( file.menu , "BtnModeListSlider" ) + var sliderPanel = Hud_GetChild( file.menu , "BtnModeListSliderPanel" ) + var movementCapture = Hud_GetChild( file.menu , "MouseMovementCapture" ) + + int[2] screenSize = GetScreenSize() + float maxHeight = 596.0 * ( screenSize[1] / 1080.0 ) + float minHeight = 80.0 * ( screenSize[1] / 1080.0 ) + + float height = maxHeight * ( MODES_PER_PAGE / modes ) + + if ( height > maxHeight ) height = maxHeight + if ( height < minHeight ) height = minHeight + + Hud_SetHeight( sliderButton, height ) + Hud_SetHeight( sliderPanel, height ) + Hud_SetHeight( movementCapture, height ) +} + + +void function UpdateListSliderPosition( int modes ) +{ + if( modes < MODES_PER_PAGE ) + return + + var sliderButton = Hud_GetChild( file.menu, "BtnModeListSlider" ) + var sliderPanel = Hud_GetChild( file.menu, "BtnModeListSliderPanel" ) + var movementCapture = Hud_GetChild( file.menu, "MouseMovementCapture" ) + + float minYPos = -40.0 * ( GetScreenSize()[1] / 1080.0 ) + float useableSpace = (596.0 * ( GetScreenSize()[1] / 1080.0 ) - Hud_GetHeight( sliderPanel ) ) + + float jump = minYPos - ( useableSpace / ( float( modes ) - MODES_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 OnScrollDown( var button ) +{ + if (file.sortedModes.len() <= MODES_PER_PAGE) return + file.scrollOffset += 5 + if (file.scrollOffset + MODES_PER_PAGE > file.sortedModes.len()) { + file.scrollOffset = file.sortedModes.len() - MODES_PER_PAGE + } + UpdateVisibleModes() + UpdateListSliderPosition( file.sortedModes.len() ) +} + +void function OnScrollUp( var button ) +{ + file.scrollOffset -= 5 + if ( file.scrollOffset < 0 ) { + file.scrollOffset = 0 + } + UpdateVisibleModes() + UpdateListSliderPosition( file.sortedModes.len() ) +} + +void function OnDownArrowSelected( var button ) +{ + if ( file.sortedModes.len() <= MODES_PER_PAGE ) return + file.scrollOffset += 1 + if ( file.scrollOffset + MODES_PER_PAGE > file.sortedModes.len() ) + { + file.scrollOffset = file.sortedModes.len() - MODES_PER_PAGE + } + + UpdateVisibleModes() + UpdateListSliderPosition( file.sortedModes.len() ) +} + + +void function OnUpArrowSelected( var button ) +{ + file.scrollOffset -= 1 + if ( file.scrollOffset < 0 ) + { + file.scrollOffset = 0 + } + + UpdateVisibleModes() + UpdateListSliderPosition( file.sortedModes.len() ) +} + +bool function IsStringCategory( string str ) +{ + return GetGameModeDisplayName( str ) == "" +} + +///////////////////////////// +// LIST +///////////////////////////// + void function UpdateVisibleModes() -{ +{ // ensures that we only ever show enough buttons for the number of modes we have - array buttons = GetElementsByClassname( GetMenu( "ModesMenu" ), "ModeButton" ) - foreach ( var button in buttons ) + array buttons = GetElementsByClassname( GetMenu( "ModesMenu" ), "ModeSelectorPanel" ) + foreach ( var panel in buttons ) { - Hud_SetEnabled( button, false ) - Hud_SetVisible( button, false ) + Hud_SetEnabled( panel, false ) + Hud_SetVisible( panel, false ) + Hud_SetText( Hud_GetChild( panel, "Header" ), "" ) + Hud_SetText( Hud_GetChild( panel, "BtnMode" ), "" ) + SetButtonRuiText( Hud_GetChild( panel, "BtnMode" ), "" ) } - - array modesArray = GetPrivateMatchModes() + for ( int i = 0; i < MODES_PER_PAGE; i++ ) { - if ( i + ( file.currentModePage * MODES_PER_PAGE ) >= modesArray.len() ) + if ( i + file.scrollOffset >= file.sortedModes.len() ) break - - int modeIndex = i + ( file.currentModePage * MODES_PER_PAGE ) - SetButtonRuiText( buttons[ i ], GetGameModeDisplayName( modesArray[ modeIndex ] ) ) - - Hud_SetEnabled( buttons[ i ], true ) - Hud_SetVisible( buttons[ i ], true ) - - // 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 if( IsFDMode( modesArray[ i ] ) ) - Hud_SetLocked( buttons[ i ], false ) + + // Setup locals + var panel = buttons[i] + var button = Hud_GetChild( panel, "BtnMode" ) + var header = Hud_GetChild( panel, "Header" ) + + int modeIndex = i + file.scrollOffset + string mode = file.sortedModes[ modeIndex ] + + bool bIsCategory = IsStringCategory( mode ) + mode = bIsCategory ? mode : GetGameModeDisplayName( mode ) + + // Show the panel + Hud_SetEnabled( panel, true ) + Hud_SetVisible( panel, true ) + Hud_SetLocked( button, false ) + + if( bIsCategory ) + { + Hud_SetText( header, mode ) + Hud_SetEnabled( button, false ) + } else - Hud_SetLocked( buttons[ i ], true ) + { + Hud_SetEnabled( button, true ) + SetButtonRuiText( button, mode ) + + if( blockedModes.contains( file.sortedModes[ modeIndex ] ) ) + Hud_SetLocked( button, true ) + + if ( PrivateMatch_IsValidMapModeCombo( PrivateMatch_GetSelectedMap(), mode ) ) + { + Hud_SetLocked( button, true ) + SetButtonRuiText( button, mode ) + } + } } } void function ModeButton_GetFocus( var button ) { - int modeId = int( Hud_GetScriptID( button ) ) + ( file.currentModePage * MODES_PER_PAGE ) - - var menu = GetMenu( "ModesMenu" ) - var nextModeImage = Hud_GetChild( menu, "NextModeImage" ) - var nextModeIcon = Hud_GetChild( menu, "ModeIconImage" ) - var nextModeName = Hud_GetChild( menu, "NextModeName" ) - var nextModeDesc = Hud_GetChild( menu, "NextModeDesc" ) + int modeId = int( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 - array modesArray = GetPrivateMatchModes() + var nextModeImage = Hud_GetChild( file.menu, "NextModeImage" ) + var nextModeIcon = Hud_GetChild( file.menu, "ModeIconImage" ) + var nextModeName = Hud_GetChild( file.menu, "NextModeName" ) + var nextModeDesc = Hud_GetChild( file.menu, "NextModeDesc" ) - if ( modeId > modesArray.len() ) + if ( modeId > file.sortedModes.len() ) return - string modeName = modesArray[modeId] + string modeName = file.sortedModes[modeId] asset playlistImage = GetPlaylistImage( modeName ) RuiSetImage( Hud_GetRui( nextModeImage ), "basicImage", playlistImage ) @@ -101,35 +569,17 @@ void function ModeButton_Click( var button ) if ( Hud_IsLocked( button ) ) return - int modeID = int( Hud_GetScriptID( button ) ) + ( file.currentModePage * MODES_PER_PAGE ) + int modeID = int( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 - array modesArray = GetPrivateMatchModes() - string modeName = modesArray[ modeID ] + string modeName = file.sortedModes[ modeID ] // on modded servers set us to the first map for that mode automatically // need this for coliseum mainly which is literally impossible to select without this - if ( !PrivateMatch_IsValidMapModeCombo( PrivateMatch_GetSelectedMap(), modesArray[ modeID ] ) ) + if ( !PrivateMatch_IsValidMapModeCombo( PrivateMatch_GetSelectedMap(), modeName ) ) + { ClientCommand( "SetCustomMap " + GetPrivateMatchMapsForMode( modeName )[ 0 ] ) - + } // set it ClientCommand( "PrivateMatchSetMode " + modeName ) CloseActiveMenu() } - -void function CycleModesBack( var button ) -{ - if ( file.currentModePage == 0 ) - return - - file.currentModePage-- - UpdateVisibleModes() -} - -void function CycleModesForward( var button ) -{ - if ( ( file.currentModePage + 1 ) * MODES_PER_PAGE >= GetPrivateMatchModes().len() ) - return - - file.currentModePage++ - UpdateVisibleModes() -} \ No newline at end of file diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut index 4968714c7..09001f57b 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_moddownload.nut @@ -9,6 +9,7 @@ global enum eModInstallStatus CHECKSUMING, EXTRACTING, DONE, + ABORTED, FAILED, FAILED_READING_ARCHIVE, FAILED_WRITING_TO_DISK, @@ -57,6 +58,11 @@ bool function DownloadMod( RequiredModInfo mod ) dialogData.message = Localize( "#DOWNLOADING_MOD_TEXT", mod.name, mod.version ) dialogData.showSpinner = true; + // Prevent download button + AddDialogButton( dialogData, "#CANCEL", void function() { + NSCancelModDownload() + }) + // Prevent user from closing dialog dialogData.forceChoice = true; OpenDialog( dialogData ) @@ -77,6 +83,13 @@ bool function DownloadMod( RequiredModInfo mod ) WaitFrame() } + // If download was aborted, don't close UI since it was closed by clicking cancel button + if ( state.status == eModInstallStatus.ABORTED ) + { + print("Mod download was cancelled by the user.") + return false; + } + printt( "Mod status:", state.status ) // Close loading dialog @@ -114,6 +127,12 @@ void function DisplayModDownloadErrorDialog( string modName ) { ModInstallState state = NSGetModInstallState() + // If user cancelled download, no need to display an error message + if ( state.status == eModInstallStatus.ABORTED ) + { + return + } + DialogData dialogData dialogData.header = Localize( "#FAILED_DOWNLOADING", modName ) dialogData.image = $"ui/menu/common/dialog_error" diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut index f08d69a72..67a184312 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut @@ -5,17 +5,8 @@ global function AddNorthstarModMenu_MainMenuFooter global function ReloadMods -struct modData { - string name = "" - string version = "" - string link = "" - int loadPriority = 0 - bool enabled = false - array conVars = [] -} - struct panelContent { - modData& mod + ModInfo& mod bool isHeader = false } @@ -37,10 +28,10 @@ struct { var menu array panels int scrollOffset = 0 - array enabledMods + array enabledMods var currentButton string searchTerm - modData& lastMod + ModInfo& lastMod } file const int PANELS_LEN = 15 @@ -150,11 +141,22 @@ void function OnModMenuClosed() } catch ( ex ) {} - array current = GetEnabledModsArray() + array current = GetEnabledModsArray() bool reload - foreach ( string mod in current ) + foreach ( ModInfo mod in current ) { - if ( file.enabledMods.find(mod) == -1 ) + bool notFound = true + + foreach ( ModInfo enMod in file.enabledMods ) + { + if ( mod.name == enMod.name ) + { + notFound = false + break + } + } + + if ( notFound ) { reload = true break @@ -176,10 +178,10 @@ void function OnModButtonFocused( var button ) RuiSetGameTime( rui, "startTime", -99999.99 ) // make sure it skips the whole animation for showing this RuiSetString( rui, "headerText", modName ) - RuiSetString( rui, "messageText", FormatModDescription( modName ) ) + RuiSetString( rui, "messageText", FormatModDescription() ) // Add a button to open the link with if required - string link = NSGetModDownloadLinkByModName( modName ) + string link = file.lastMod.downloadLink var linkButton = Hud_GetChild( file.menu, "ModPageButton" ) if ( link.len() ) { @@ -193,28 +195,44 @@ void function OnModButtonFocused( var button ) Hud_SetVisible( linkButton, false ) } - SetControlBarColor( modName ) + SetControlBarColor( file.lastMod ) - bool required = NSIsModRequiredOnClient( modName ) + bool required = file.lastMod.requiredOnClient Hud_SetVisible( Hud_GetChild( file.menu, "WarningLegendLabel" ), required ) Hud_SetVisible( Hud_GetChild( file.menu, "WarningLegendImage" ), required ) } void function OnModButtonPressed( var button ) { - string modName = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 ].mod.name - if ( StaticFind( modName ) && NSIsModEnabled( modName ) ) + ModInfo mod = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 ].mod + string modName = mod.name + if ( StaticFind( modName ) && mod.enabled ) CoreModToggleDialog( modName ) else { - NSSetModEnabled( modName, !NSIsModEnabled( modName ) ) - var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) - 1 ] - SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modName ) - SetControlBarColor( modName ) - SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modName ) - // RefreshMods() - UpdateListSliderPosition() - UpdateListSliderHeight() + NSSetModEnabled( modName, !mod.enabled ) + + // retrieve state of the mod that just got toggled + array infos = NSGetModInformation( mod.name ) + foreach ( modInfo in infos ) + { + if ( modInfo.name != modName || modInfo.version != mod.version ) + { + continue + } + + // Update UI mod state + file.mods[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) + file.scrollOffset - 1 ].mod = modInfo + + var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( button ) ) ) - 1 ] + SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modInfo ) + SetControlBarColor( modInfo ) + SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modInfo ) + // RefreshMods() + UpdateListSliderPosition() + UpdateListSliderHeight() + break + } } } @@ -230,8 +248,8 @@ void function OnAuthenticationAgreementButtonPressed( var button ) void function OnModLinkButtonPressed( var button ) { - string modName = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod.name - string link = NSGetModDownloadLinkByModName( modName ) + ModInfo mod = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod + string link = mod.downloadLink if ( link.find("http://") != 0 && link.find("https://") != 0 ) link = "http://" + link // links without the http or https protocol get opened in the internal browser LaunchExternalWebBrowser( link, WEBBROWSER_FLAG_FORCEEXTERNAL ) @@ -261,7 +279,7 @@ void function OnHideConVarsChange( var n ) if ( modName == "" ) return var rui = Hud_GetRui( Hud_GetChild( file.menu, "LabelDetails" ) ) - RuiSetString( rui, "messageText", FormatModDescription( modName ) ) + RuiSetString( rui, "messageText", FormatModDescription() ) } // LIST LOGIC @@ -284,23 +302,35 @@ void function CoreModToggleDialog( string mod ) void function DisableMod() { - string modName = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod.name + ModInfo mod = file.mods[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) + file.scrollOffset - 1 ].mod + string modName = mod.name NSSetModEnabled( modName, false ) - var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) - 1] - SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modName ) - SetControlBarColor( modName ) - SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modName ) + // retrieve state of the mod that just got toggled + array infos = NSGetModInformation( mod.name ) + foreach ( modInfo in infos ) + { + if ( modInfo.name != modName || modInfo.version != mod.version ) + { + continue + } + + var panel = file.panels[ int ( Hud_GetScriptID( Hud_GetParent( file.currentButton ) ) ) - 1] + SetControlBoxColor( Hud_GetChild( panel, "ControlBox" ), modInfo ) + SetControlBarColor( modInfo ) + SetModEnabledHelperImageAsset( Hud_GetChild( panel, "EnabledImage" ), modInfo ) - RefreshMods() + RefreshMods() + break + } } -array function GetEnabledModsArray() +array function GetEnabledModsArray() { - array enabledMods - foreach ( string mod in NSGetModNames() ) + array enabledMods + foreach ( ModInfo mod in NSGetModsInformation() ) { - if ( NSIsModEnabled( mod ) ) + if ( mod.enabled ) enabledMods.append( mod ) } return enabledMods @@ -324,29 +354,30 @@ void function UpdateList() void function RefreshMods() { - array modNames = NSGetModNames() + array mods = NSGetModsInformation() file.mods.clear() bool reverse = GetConVarBool( "modlist_reverse" ) - int lastLoadPriority = reverse ? NSGetModLoadPriority( modNames[ modNames.len() - 1 ] ) + 1 : -1 + int lastLoadPriority = reverse ? mods.top().loadPriority + 1 : -1 string searchTerm = Hud_GetUTF8Text( Hud_GetChild( file.menu, "BtnModsSearch" ) ).tolower() - for ( int i = reverse ? modNames.len() - 1 : 0; - reverse ? ( i >= 0 ) : ( i < modNames.len() ); + for ( int i = reverse ? mods.len() - 1 : 0; + reverse ? ( i >= 0 ) : ( i < mods.len() ); i += ( reverse ? -1 : 1) ) { - string mod = modNames[i] + ModInfo mod = mods[i] + string modName = mod.name // Do not display remote mods - if ( NSIsModRemote( mod ) ) + if ( mod.isRemote ) continue - if ( searchTerm.len() && mod.tolower().find( searchTerm ) == null ) + if ( searchTerm.len() && modName.tolower().find( searchTerm ) == null ) continue - bool enabled = NSIsModEnabled( mod ) - bool required = NSIsModRequiredOnClient( mod ) + bool enabled = mod.enabled + bool required = mod.requiredOnClient switch ( GetConVarInt( "filter_mods" ) ) { case filterShow.ONLY_ENABLED: @@ -367,11 +398,11 @@ void function RefreshMods() break } - int pr = NSGetModLoadPriority( mod ) + int pr = mod.loadPriority if ( reverse ? pr < lastLoadPriority : pr > lastLoadPriority ) { - modData m + ModInfo m m.name = pr.tostring() panelContent c @@ -381,16 +412,8 @@ void function RefreshMods() lastLoadPriority = pr } - modData m - m.name = mod - m.version = NSGetModVersionByModName( mod ) - m.link = NSGetModDownloadLinkByModName( mod ) - m.loadPriority = NSGetModLoadPriority( mod ) - m.enabled = enabled - m.conVars = NSGetModConvarsByModName( mod ) - panelContent c - c.mod = m + c.mod = mod file.mods.append( c ) } @@ -404,7 +427,7 @@ void function DisplayModPanels() break panelContent c = file.mods[ file.scrollOffset + i ] - modData mod = c.mod + ModInfo mod = c.mod var btn = Hud_GetChild( panel, "BtnMod" ) var headerLabel = Hud_GetChild( panel, "Header" ) var box = Hud_GetChild( panel, "ControlBox" ) @@ -434,53 +457,45 @@ void function DisplayModPanels() Hud_SetVisible( headerLabel, false ) - SetControlBoxColor( box, mod.name ) + SetControlBoxColor( box, mod ) Hud_SetVisible( box, true ) Hud_SetVisible( line, false ) - Hud_SetVisible( warning, NSIsModRequiredOnClient( c.mod.name ) ) + Hud_SetVisible( warning, mod.requiredOnClient ) - SetModEnabledHelperImageAsset( enabledImage, c.mod.name ) + SetModEnabledHelperImageAsset( enabledImage, c.mod ) } Hud_SetVisible( panel, true ) } } -void function SetModEnabledHelperImageAsset( var panel, string modName ) +void function SetModEnabledHelperImageAsset( var panel, ModInfo mod ) { - if( NSIsModEnabled( modName ) ) + if( mod.enabled ) RuiSetImage( Hud_GetRui( panel ), "basicImage", $"rui/menu/common/merit_state_success" ) else RuiSetImage( Hud_GetRui( panel ), "basicImage", $"rui/menu/common/merit_state_failure" ) - RuiSetFloat3(Hud_GetRui( panel ), "basicImageColor", GetControlColorForMod( modName ) ) + RuiSetFloat3(Hud_GetRui( panel ), "basicImageColor", GetControlColorForMod( mod ) ) Hud_SetVisible( panel, true ) } -void function SetControlBoxColor( var box, string modName ) +void function SetControlBoxColor( var box, ModInfo mod ) { var rui = Hud_GetRui( box ) - // if ( NSIsModEnabled( modName ) ) - // RuiSetFloat3(rui, "basicImageColor", <0,1,0>) - // else - // RuiSetFloat3(rui, "basicImageColor", <1,0,0>) - RuiSetFloat3(rui, "basicImageColor", GetControlColorForMod( modName ) ) + RuiSetFloat3(rui, "basicImageColor", GetControlColorForMod( mod ) ) } -void function SetControlBarColor( string modName ) +void function SetControlBarColor( ModInfo mod ) { var bar_element = Hud_GetChild( file.menu, "ModEnabledBar" ) var bar = Hud_GetRui( bar_element ) - // if ( NSIsModEnabled( modName ) ) - // RuiSetFloat3(bar, "basicImageColor", <0,1,0>) - // else - // RuiSetFloat3(bar, "basicImageColor", <1,0,0>) - RuiSetFloat3(bar, "basicImageColor", GetControlColorForMod( modName ) ) + RuiSetFloat3(bar, "basicImageColor", GetControlColorForMod( mod ) ) Hud_SetVisible( bar_element, true ) } -vector function GetControlColorForMod( string modName ) +vector function GetControlColorForMod( ModInfo mod ) { - if ( NSIsModEnabled( modName ) ) + if ( mod.enabled ) switch ( GetConVarInt( "colorblind_mode" ) ) { case 1: @@ -502,17 +517,20 @@ vector function GetControlColorForMod( string modName ) unreachable } -string function FormatModDescription( string modName ) +string function FormatModDescription() { + ModInfo mod = file.lastMod + string modName = mod.name + string ret // version - ret += format( "Version %s\n", NSGetModVersionByModName( modName ) ) + ret += format( "Version %s\n", mod.version ) // load priority - ret += format( "Load Priority: %i\n", NSGetModLoadPriority( modName ) ) + ret += format( "Load Priority: %i\n", mod.loadPriority ) // convars - array modCvars = NSGetModConvarsByModName( modName ) + array modCvars = mod.conVars if ( modCvars.len() != 0 && GetConVarBool( "modlist_show_convars" ) ) { ret += "ConVars: " @@ -529,7 +547,7 @@ string function FormatModDescription( string modName ) } // description - ret += format( "\n%s\n", NSGetModDescriptionByModName( modName ) ) + ret += format( "\n%s\n", mod.description ) return ret } 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 cdeb8b3e0..f2effd128 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_serverbrowser.nut @@ -973,18 +973,27 @@ void function OnServerSelected_Threaded( var button ) foreach ( requiredModInfo in server.requiredMods ) { // Tolerate core mods having different versions - if ( requiredModInfo.name.len() > 10 && requiredModInfo.name.slice(0, 10) == "Northstar." ) + if ( IsCoreMod( requiredModInfo.name ) ) continue if ( !modNames.contains( requiredModInfo.name ) ) { - print( format ( "\"%s\" was not found locally, triggering manifesto fetching.", requiredModInfo.name ) ) - uninstalledModFound = true - break - } else if ( NSGetModVersionByModName( requiredModInfo.name ) != requiredModInfo.version ) { - print( format ( "\"%s\" was found locally but has version \"%s\" while server requires \"%s\", triggering manifesto fetching.", requiredModInfo.name, NSGetModVersionByModName( requiredModInfo.name ), requiredModInfo.version ) ) + print( format ( "\"%s\" was not found locally" + ( autoDownloadAllowed ? ", triggering manifesto fetching." : "." ), requiredModInfo.name ) ) uninstalledModFound = true break + } else { + array modVersions = GetModVersions( requiredModInfo.name ) + + if ( !modVersions.contains( requiredModInfo.version ) ) { + print( format ( "\"%s\" was found locally but has versions:", requiredModInfo.name ) ) + foreach ( string version in modVersions ) + { + print(" - " + version) + } + print( format ( "while server requires \"%s\"" + ( autoDownloadAllowed ? ", triggering manifesto fetching." : "." ), requiredModInfo.version ) ) + uninstalledModFound = true + break + } } } @@ -992,17 +1001,16 @@ void function OnServerSelected_Threaded( var button ) // mods can be installed through auto-download if ( uninstalledModFound && autoDownloadAllowed ) { - print("Auto-download is allowed, checking if missing mods can be installed automatically.") FetchVerifiedModsManifesto() } foreach ( RequiredModInfo mod in server.requiredMods ) { // Tolerate core mods having different versions - if ( mod.name.len() > 10 && mod.name.slice(0, 10) == "Northstar." ) + if ( IsCoreMod( mod.name ) ) continue - if ( !NSGetModNames().contains( mod.name ) || NSGetModVersionByModName( mod.name ) != mod.version ) + if ( !NSGetModNames().contains( mod.name ) || !GetModVersions( mod.name ).contains( mod.version ) ) { // Auto-download mod if ( autoDownloadAllowed ) @@ -1055,43 +1063,8 @@ void function OnServerSelected_Threaded( var button ) return } } - else - { - // this uses semver https://semver.org - array serverModVersion = split( mod.name, "." ) - array clientModVersion = split( NSGetModVersionByModName( mod.name ), "." ) - - bool semverFail = false - // if server has invalid semver don't bother checking - if ( serverModVersion.len() == 3 ) - { - // bad client semver - if ( clientModVersion.len() != serverModVersion.len() ) - semverFail = true - // major version, don't think we should need to check other versions - else if ( clientModVersion[ 0 ] != serverModVersion[ 0 ] ) - semverFail = true - } - - if ( semverFail ) - { - DialogData dialogData - dialogData.header = "#ERROR" - dialogData.message = Localize( "#WRONG_MOD_VERSION", mod.name, mod.version, NSGetModVersionByModName( mod.name ) ) - dialogData.image = $"ui/menu/common/dialog_error" - - #if PC_PROG - AddDialogButton( dialogData, "#DISMISS" ) - AddDialogFooter( dialogData, "#A_BUTTON_SELECT" ) - #endif // PC_PROG - AddDialogFooter( dialogData, "#B_BUTTON_DISMISS_RUI" ) - - OpenDialog( dialogData ) - - return - } - } + // If we get here, means that mod version exists locally => we good } if ( server.requiresPassword ) @@ -1136,25 +1109,31 @@ void function ThreadedAuthAndConnectToServer( string password = "", bool modsCha if ( NSWasAuthSuccessful() ) { // disable all RequiredOnClient mods that are not required by the server and are currently enabled - foreach ( string modName in NSGetModNames() ) + foreach ( ModInfo mod in NSGetModsInformation() ) { - if ( NSIsModRequiredOnClient( modName ) && NSIsModEnabled( modName ) ) + string modName = mod.name + string modVersion = mod.version + + if ( mod.requiredOnClient && mod.enabled ) { // 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) + // this tolerates a version difference for requiredOnClient core mods (only Northstar.Custom for now) + if (mod.name == modName && ( IsCoreMod( modName ) || mod.version == modVersion )) { found = true + print(format("\"%s\" (v%s) is required and already enabled.", modName, modVersion)) break } } - // if we didnt find the mod name, disable the mod + // if we didn't find the mod name, disable the mod if (!found) { modsChanged = true NSSetModEnabled( modName, false ) + print(format("Disabled \"%s\" (v%s) since it's not required on server.", modName, modVersion)) } } } @@ -1162,10 +1141,33 @@ void function ThreadedAuthAndConnectToServer( string password = "", bool modsCha // enable all RequiredOnClient mods that are required by the server and are currently disabled foreach ( RequiredModInfo mod in file.lastSelectedServer.requiredMods ) { - if ( NSIsModRequiredOnClient( mod.name ) && !NSIsModEnabled( mod.name )) + string modName = mod.name + string modVersion = mod.version + array localModInfos = NSGetModInformation( modName ) + + // Tolerate core mods (only Northstar.Custom for now) having a different version than server + if ( IsCoreMod(modName) ) + { + if ( !localModInfos[0].enabled ) + { + modsChanged = true + NSSetModEnabled( modName, true ) + print(format("Enabled \"%s\" (v%s) to join server.", modName, localModInfos[0].version)) + } + } + + else { - modsChanged = true - NSSetModEnabled( mod.name, true ) + foreach( localMod in localModInfos ) + { + if ( localMod.version == mod.version ) + { + modsChanged = true + NSSetModEnabled( mod.name, true ) + print(format("Enabled \"%s\" (v%s) to join server.", modName, modVersion)) + break + } + } } } @@ -1361,3 +1363,19 @@ void function TriggerConnectToServerCallbacks( ServerInfo ornull targetServer = callback( expect ServerInfo( targetServer ) ) } } + +const array CORE_MODS = ["Northstar.Client", "Northstar.Coop", "Northstar.CustomServers", "Northstar.Custom"] +bool function IsCoreMod( string modName ) +{ + return CORE_MODS.find( modName ) != -1 +} + +array function GetModVersions( string modName ) +{ + array versions = [] + foreach ( ModInfo mod in NSGetModInformation( modName ) ) + { + versions.append( mod.version ) + } + return versions +} diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut index 6dbafde96..afba8a70d 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_setversionlabel.nut @@ -5,6 +5,6 @@ void function NS_SetVersionLabel() { var mainMenu = GetMenu( "MainMenu" ) //Gets main menu element var versionLabel = GetElementsByClassname( mainMenu, "nsVersionClass" )[0] //Gets the label from the mainMenu element. - Hud_SetText( versionLabel, "v" + NSGetModVersionByModName("Northstar.Client")) //Sets the label text (Getting Northstar version from Northstar.Client) + Hud_SetText( versionLabel, "v" + NSGetModInformation( "Northstar.Client" )[0].version ) //Sets the label text (Getting Northstar version from Northstar.Client) } diff --git a/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut b/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut index 2f1bcf025..330cd0d6a 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/panel_mainmenu.nut @@ -431,9 +431,9 @@ void function UpdatePlayButton( var button ) { // restrict non-vanilla players from accessing official servers bool hasNonVanillaMods = false - foreach ( string modName in NSGetModNames() ) + foreach ( ModInfo mod in NSGetModsInformation() ) { - if ( NSIsModEnabled( modName ) && NSIsModRequiredOnClient( modName ) ) + if ( mod.enabled && mod.requiredOnClient ) { hasNonVanillaMods = true break diff --git a/Northstar.Custom/keyvalues/scripts/aisettings/npc_pilot_elite.txt b/Northstar.Custom/keyvalues/scripts/aisettings/npc_pilot_elite.txt new file mode 100644 index 000000000..349277c39 --- /dev/null +++ b/Northstar.Custom/keyvalues/scripts/aisettings/npc_pilot_elite.txt @@ -0,0 +1,5 @@ +npc_pilot_elite +{ + GibModel0 "models/gibs/human_gibs.mdl" + headshotFX "P_headshot_pilot" +} \ No newline at end of file diff --git a/Northstar.Custom/mod/models/weapons/titan_triple_threat_og/w_titan_triple_threat_og.mdl b/Northstar.Custom/mod/models/weapons/titan_triple_threat_og/w_titan_triple_threat_og.mdl index b955e1c02..2394fdd20 100644 Binary files a/Northstar.Custom/mod/models/weapons/titan_triple_threat_og/w_titan_triple_threat_og.mdl and b/Northstar.Custom/mod/models/weapons/titan_triple_threat_og/w_titan_triple_threat_og.mdl differ diff --git a/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut b/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut index 37d4356f0..10bca342c 100644 --- a/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut @@ -207,7 +207,7 @@ string function GetSelectedBurnCardRef( entity player ) #if SERVER if ( GetItemDisplayData( ref ).hidden ) - ClientCommand( player, "disconnect" ) + NSDisconnectPlayer( player, "" ) #endif #if SERVER || CLIENT diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut index 6729ff975..c3bdd4849 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut @@ -11,6 +11,7 @@ void function GamemodeHidden_Init() SetLoadoutGracePeriodEnabled( false ) // prevent modifying loadouts with grace period SetWeaponDropsEnabled( false ) SetRespawnsEnabled( false ) + SetGamemodeAllowsTeamSwitch( false ) Riff_ForceTitanAvailability( eTitanAvailability.Never ) Riff_ForceBoostAvailability( eBoostAvailability.Disabled ) Riff_ForceSetEliminationMode( eEliminationMode.Pilots ) diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut index 02f0799a1..e03f01adf 100644 --- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut @@ -11,6 +11,7 @@ void function GamemodeInfection_Init() SetSpawnpointGamemodeOverride( FFA ) SetLoadoutGracePeriodEnabled( false ) // prevent modifying loadouts with grace period SetWeaponDropsEnabled( false ) + SetGamemodeAllowsTeamSwitch( false ) SetShouldUseRoundWinningKillReplay( true ) Riff_ForceTitanAvailability( eTitanAvailability.Never ) Riff_ForceBoostAvailability( eBoostAvailability.Disabled ) diff --git a/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut index 95ab39158..89ea38f03 100644 --- a/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut @@ -378,11 +378,6 @@ bool function CodeCallback_IsValidMeleeAttackTarget( entity attacker, entity tar 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 ) ) { @@ -410,24 +405,10 @@ void function CodeCallback_OnMeleePressed( entity player ) } 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 ) } @@ -527,14 +508,7 @@ bool function PlayerTriesSyncedMelee( entity player, entity target ) } 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( @@ -969,10 +943,28 @@ string function GetVictimSyncedMeleeTargetType( entity ent ) { targetType = "prowler" } + + // Disabled to allow for executing NPC Pilots + + // The way this function works, is that if an entity gets added here, then whatever "targetType" it returns + // gets passed to sh_melee_synced_human.gnut or sh_melee_synced_titan.gnut, + // which determines what animation set that entity should use when getting executed + // If an entity is not included, the it uses its BodyType to determine it ( see line 83 in sh_melee_synced_human) + // As you can see: Grunts, and Spectres are not included here, so it gets their BodyType (human) and goes from there + + // I imagine the reason Respawn added the NPC Pilots to this function, is so they could make unique executions for them, + // just like the Prowlers + // Unfortunately, they either scrapped them, or simply never got around to making them + // That means this piece of code has basically no reason to exist anymore + // The only thing it does is break executions and thats it + // Since NPC Pilots also use the "human" BodyType, it means we can execute them with no issues + +/* else if ( IsPilotElite( ent ) ) { targetType = "pilotelite" } +*/ else if ( ent.IsNPC() ) { targetType = ent.GetBodyType() @@ -1006,11 +998,7 @@ SyncedMeleeChooser ornull function GetSyncedMeleeChooserForPlayerVsTarget( entit 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() ) @@ -1223,4 +1211,4 @@ SyncedMelee ornull function PickRandomExecution( SyncedMeleeChooser actions, ent return possibleExecutions[0] } #endif -#endif \ No newline at end of file +#endif diff --git a/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee_human.gnut b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee_human.gnut new file mode 100644 index 000000000..031acacfa --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee_human.gnut @@ -0,0 +1,490 @@ +untyped + +global function MeleeHumanShared_Init + +global function HumanUnsyncedMelee +global function HumanMeleeAttack + +function MeleeHumanShared_Init() +{ + PrecacheParticleSystem( $"P_melee_player" ) + RegisterSignal( "StopSlowMoMelee" ) + RegisterSignal( "StopHighlightValidMeleeEnemy" ) +} + +function HumanUnsyncedMelee( entity player, bool movestunBlocked ) +{ + entity activeWeapon = player.GetActiveWeapon() + if ( !IsValid( activeWeapon ) ) + { +#if SERVER + print( "SERVER: " + player + " has no valid active weapon\n" ) +#else + print( "CLIENT: " + player + " has no valid active weapon\n" ) +#endif + return + } + + entity meleeWeapon = player.GetMeleeWeapon() + if ( !IsValid( meleeWeapon ) ) + { +#if SERVER + print( "SERVER: " + player + " has no valid melee weapon\n" ) +#else + print( "CLIENT: " + player + " has no valid melee weapon\n" ) +#endif + return + } + + local meleeAttackType = PLAYER_MELEE_STATE_HUMAN_KICK_ATTACK + if ( activeWeapon.GetWeaponClassName() == "mp_weapon_dash_melee" ) + meleeAttackType = PLAYER_MELEE_STATE_HUMAN_EVISCERATE_ATTACK + + player.PlayerMelee_StartAttack( meleeAttackType ) + + if ( player.PlayerMelee_GetState() == PLAYER_MELEE_STATE_HUMAN_EVISCERATE_ATTACK ) + { + vector lungeTargetPos = (player.GetOrigin() + (player.GetViewVector() * 300)) + player.Lunge_SetTargetPosition( lungeTargetPos ) + player.Lunge_EnableFlying() + } + else + { + entity lungeTarget = GetLungeTargetForPlayer( player ) + if ( IsAlive( lungeTarget ) ) + { + if ( !movestunBlocked ) + { + if ( player.Lunge_SetTargetEntity( lungeTarget, true ) ) + { + if ( lungeTarget.IsTitan() ) + { + player.Lunge_EnableFlying() + vector oldOffset = player.Lunge_GetEndPositionOffset() + player.Lunge_SetEndPositionOffset( oldOffset + <0, 0, 128> ) + } + else + { + if ( player.IsOnGround() ) + player.Lunge_LockPitch( true ) + } + } + } + } +#if SERVER + // if we don't lunge at anything stop slowmo + else if ( IsSingleplayer() && PROTO_IsSlowMoWeapon( meleeWeapon ) ) + { + player.Signal( "StopSlowMoMelee" ) + } +#endif // #if SERVER + } + +#if SERVER + meleeWeapon.EmitWeaponNpcSound_DontUpdateLastFiredTime( 200, 0.2 ) +#endif // #if SERVER + + //player.Weapon_StartCustomActivity( meleeActivity1p, false ) + player.SetSelectedOffhandToMelee() +} + +function DoReactionForTitanHit( entity player, entity titan ) +{ + player.Lunge_SetTargetEntity( titan, true ) + if ( player.Lunge_IsLungingToEntity() ) + player.Lunge_EnableFlying() + + vector titanCenter = titan.EyePosition() + vector delta = (player.EyePosition() - titanCenter) + vector dir = Normalize( delta ) + player.Lunge_SetEndPositionOffset( dir * 350 ) +} + +function HumanMeleeAttack( entity player ) +{ + if ( player.IsPhaseShifted() ) + return + if ( player.PlayerMelee_GetAttackHitEntity() ) + return + if ( IsInExecutionMeleeState( player ) ) + return + + entity meleeWeapon = player.GetMeleeWeapon() + float attackRange = meleeWeapon.GetMeleeAttackRange() + + if ( player.Lunge_IsGroundExecute() ) + attackRange = 150 + + table traceResult = PlayerMelee_AttackTrace( player, attackRange, CodeCallback_IsValidMeleeAttackTarget ) + + entity hitEnt = expect entity( traceResult.ent ) + if ( !IsValid( hitEnt ) ) + return + + if ( PlayerMelee_IsServerSideEffects() ) + { +#if SERVER + vector hitNormal = Normalize( traceResult.startPosition - traceResult.position ) + player.DispatchImpactEffects( hitEnt, traceResult.startPosition, traceResult.position, hitNormal, traceResult.surfaceProp, traceResult.staticPropIndex, traceResult.damageType, meleeWeapon.GetImpactTableIndex(), player, traceResult.impactEffectFlags | IEF_SERVER_SIDE_EFFECT ) +#endif + } + else + { + vector hitNormal = Normalize( traceResult.startPosition - traceResult.position ) + player.DispatchImpactEffects( hitEnt, traceResult.startPosition, traceResult.position, hitNormal, traceResult.surfaceProp, traceResult.staticPropIndex, traceResult.damageType, meleeWeapon.GetImpactTableIndex(), player, traceResult.impactEffectFlags ) + } + + player.PlayerMelee_SetAttackHitEntity( hitEnt ) + if ( !hitEnt.IsWorld() ) + player.PlayerMelee_SetAttackRecoveryShouldBeQuick( true ) + + if ( hitEnt.IsTitan() ) + DoReactionForTitanHit( player, hitEnt ) + + if ( hitEnt.IsBreakableGlass() ) + { +#if SERVER + hitEnt.BreakSphere( traceResult.position, 50 ) +#endif // #if SERVER + } + else + { + if ( player.IsInputCommandHeld( IN_MELEE ) && AttemptHumanMeleeExecution( player, hitEnt, meleeWeapon, traceResult ) ) + return + +#if CLIENT + //MeleeImpactFX( player, meleeWeapon, hitEnt ) +#else + HumanMeleeAttack_DoImpact( player, meleeWeapon, traceResult ) +#endif + const float SCALE_WHEN_ENEMY = 1.0 + const float SCALE_WHEN_NOT_ENEMY = 0.5 + float severityScale = IsEnemyTeam( player.GetTeam(), hitEnt.GetTeam() ) ? SCALE_WHEN_ENEMY : SCALE_WHEN_NOT_ENEMY + meleeWeapon.DoMeleeHitConfirmation( severityScale ) + } +} + +#if 0 //CLIENT +function MeleeImpactFX( entity player, entity meleeWeapon, entity target ) +{ + if ( !target.IsWorld() ) + { + entity cockpit = player.GetCockpit() + if ( IsValid( cockpit ) ) + StartParticleEffectOnEntity( cockpit, GetParticleSystemIndex( $"P_melee_player" ), FX_PATTACH_ABSORIGIN_FOLLOW, -1 ) //P_MFD works well too + } +} +#endif // CLIENT + +#if SERVER +function HumanMeleeAttack_DoImpact( entity player, entity meleeWeapon, traceResult ) +{ + local angles = player.EyeAngles() + entity target = expect entity( traceResult.ent ) + player.PlayerMelee_SetAttackHitEntity( target ) + + string weaponName = meleeWeapon.GetWeaponClassName() + local damageSource = eDamageSourceId[weaponName] + int damageAmount = GetDamageAmountForTarget( meleeWeapon, target ) + + if ( IsHumanSized( target ) ) + { + if ( target.IsPlayer() ) //Strip away rodeo protection + { + entity titanBeingRodeoed = GetTitanBeingRodeoed( target ) + if ( IsValid( titanBeingRodeoed ) ) + TakeAwayFriendlyRodeoPlayerProtection( titanBeingRodeoed ) + } + + // ?? + target.SetContinueAnimatingAfterRagdoll( true ) + } + + vector oldVelocity = target.GetVelocity() + vector damageForce = AnglesToForward( angles ) * meleeWeapon.GetWeaponDamageForce() + + if ( target.IsNPC() && target.CanBeGroundExecuted() ) + target.TakeDamage( target.GetHealth(), player, player, { scriptType = DF_RAGDOLL | meleeWeapon.GetWeaponDamageFlags(), damageType = DMG_MELEE_ATTACK, damageSourceId = damageSource, origin = traceResult.position, force = Vector( 0, 0, 0 ) } ) + else + target.TakeDamage( damageAmount, player, player, { scriptType = meleeWeapon.GetWeaponDamageFlags(), damageType = DMG_MELEE_ATTACK, damageSourceId = damageSource, origin = traceResult.position, force = damageForce } ) + + // PROTO DEV + if ( IsSingleplayer() ) + { + if ( PROTO_ShouldActivateSlowMo( target, meleeWeapon ) ) + { + thread PROTO_SlowMoMelee( player, target, meleeWeapon ) + } + } + + // triggers: + { + local triggerTraceDir = Normalize( traceResult.position - traceResult.startPosition ) + player.TraceAttackToTriggers( damageAmount, player, player, { scriptType = meleeWeapon.GetWeaponDamageFlags(), damageType = DMG_MELEE_ATTACK, damageSourceId = damageSource, force = damageForce }, traceResult.startPosition, traceResult.position, triggerTraceDir ) + } + + if ( target.IsPlayerDecoy() ) + { + player.PlayerMelee_EndAttack() + } +} + +int function GetDamageAmountForTarget( entity meleeWeapon, entity target ) +{ + // special case + if ( IsTurret( target ) && IsHumanSized( target ) ) + return target.GetMaxHealth() + 1 + + // default + return meleeWeapon.GetDamageAmountForArmorType( target.GetArmorType() ) +} + + +// HACK - testing linked slow mo melee +void function PROTO_SlowMoMelee( entity player, entity currentEnemy, entity meleeWeapon ) +{ + player.EndSignal( "OnDeath" ) + player.EndSignal( "OnDestroy" ) + player.EndSignal( "StopSlowMoMelee" ) + + float duration = 1.75 //1.75 + float timescale = 0.4 + float lastKillTimescale = 0.2 + + var SlowMoTimeRemaining = player.s.meleeSlowMoEndTime - Time() + + meleeWeapon.SetMods( [ "SlowMoLinked" ] ) // need to switch to the other mod to get the longer lunge range + + // find an enemy close enough that we can melee him next + entity nextEnemy = PROTO_GetNextMeleeEnemy( player, meleeWeapon, currentEnemy ) + + if ( !IsValid( nextEnemy ) ) + { + meleeWeapon.SetMods( [ "SlowMo" ] ) + if ( SlowMoTimeRemaining > 0 ) + { + // do extra slowdown for the last kill in a linked slow-mo melee chain. + ServerCommand( "host_timescale " + string( lastKillTimescale ) ) + wait 0.2 + player.Signal( "StopSlowMoMelee" ) // this will also end this thread + } + + return + } + + if ( player.s.meleeSlowMoEndTime > Time() ) + { + // if we are already in slow-mo just turn towards the next enemy and extend the duration + thread PROTO_TurnViewTowardsClosestEnemy( player, nextEnemy ) + player.s.meleeSlowMoEndTime = Time() + duration // += duration + return + } + + // require a 5 second cool down between leaving and reentering slow mo. + if ( SlowMoTimeRemaining > -5 ) + return + + thread PROTO_TurnViewTowardsClosestEnemy( player, nextEnemy ) + + // enter slow mo + ServerCommand( "host_timescale " + string( timescale ) ) + player.s.meleeSlowMoEndTime = Time() + duration + meleeWeapon.SetMods( [ "SlowMoLinked" ] ) + + float range = meleeWeapon.GetMeleeLungeTargetRange() + array enemyArray = PROTO_GetMeleeEnemiesWithinRange( player.GetOrigin(), player.GetTeam(), range ) + foreach( enemy in enemyArray ) + thread PROTO_HighlightValidMeleeEnemy( player, enemy, meleeWeapon ) + + player.SetInvulnerable() + + OnThreadEnd( + function() : ( player, meleeWeapon ) + { + if ( IsValid( meleeWeapon ) ) + meleeWeapon.SetMods( [ "SlowMo" ] ) + + if ( IsValid( player ) ) + { + player.ClearInvulnerable() + player.s.meleeSlowMoEndTime = 0 + } + + thread PROTO_EaseOutSlowMo() + } + ) + + while( Time() <= player.s.meleeSlowMoEndTime ) + { + var waitTime = player.s.meleeSlowMoEndTime - Time() + wait waitTime + } + + player.Signal( "StopSlowMoMelee" ) +} + +void function PROTO_EaseOutSlowMo() +{ + ServerCommand( "host_timescale 0.4" ) + wait 0.1 + ServerCommand( "host_timescale 0.7" ) + wait 0.1 + ServerCommand( "host_timescale 1.0" ) +} + +bool function PROTO_IsSlowMoWeapon( entity meleeWeapon ) +{ + return ( meleeWeapon.HasMod( "SlowMo" ) || meleeWeapon.HasMod( "SlowMoLinked" ) ) +} + +bool function PROTO_ShouldActivateSlowMo( entity enemy, entity meleeWeapon ) +{ + if ( !PROTO_IsSlowMoWeapon( meleeWeapon ) ) + return false + + if ( !IsHumanSized( enemy ) ) + return false + + return true +} + +void function PROTO_TurnViewTowardsClosestEnemy( entity player, entity nextEnemy ) +{ + player.EndSignal( "OnDeath" ) + + OnThreadEnd( + function() : ( player ) + { + if ( IsValid( player ) ) + { + player.ClearParent() + player.PlayerCone_Disable() + } + } + ) + + // turn player view towards next enemy + vector vec = nextEnemy.GetOrigin() - player.GetOrigin() + vector newAngles = VectorToAngles( vec ) + + entity scriptMover = CreateScriptMover( player.GetOrigin(), player.GetAngles() ) + player.SetParent( scriptMover ) + + player.PlayerCone_SetLerpTime( 0.15 ) + + player.PlayerCone_FromAnim() + player.PlayerCone_SetMinYaw( -15 ) + player.PlayerCone_SetMaxYaw( 15 ) + player.PlayerCone_SetMinPitch( -5 ) + player.PlayerCone_SetMaxPitch( 15 ) + + wait 0.2 + + scriptMover.NonPhysicsRotateTo( newAngles, 0.4, 0.2, 0.2 ) + wait 0.4 +} + +entity function PROTO_GetNextMeleeEnemy( entity player, entity meleeWeapon, entity lastEnemy ) +{ + float range = meleeWeapon.GetMeleeLungeTargetRange() + array enemyArray = PROTO_GetMeleeEnemiesWithinRange( player.GetOrigin(), player.GetTeam(), range ) + entity nextEnemy = null + + foreach ( enemy in enemyArray ) + { + float heightDif = enemy.GetOrigin().z - player.GetOrigin().z + if ( heightDif < -96 || heightDif > 48 ) + continue + + float frac = TraceLineSimple( player.EyePosition(), enemy.EyePosition(), enemy ) + if ( frac < 1 ) + continue + + if ( enemy == lastEnemy ) + continue + + nextEnemy = enemy + break + } + + return nextEnemy +} + +array function PROTO_GetMeleeEnemiesWithinRange( vector playerOrigin, int playerTeam, float range ) +{ + array enemyArray = GetNPCArrayEx( "npc_soldier", TEAM_ANY, playerTeam, playerOrigin, range ) + enemyArray.extend( GetNPCArrayEx( "npc_spectre", TEAM_ANY, playerTeam, playerOrigin, range ) ) + + return enemyArray +} + +void function PROTO_HighlightValidMeleeEnemy( entity player, entity enemy, entity meleeWeapon ) +{ + enemy.Signal( "StopHighlightValidMeleeEnemy" ) + enemy.EndSignal( "StopHighlightValidMeleeEnemy" ) + + player.EndSignal( "StopSlowMoMelee" ) + player.EndSignal( "OnDeath" ) + player.EndSignal( "OnDestroy" ) + + enemy.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( enemy ) + { + if ( IsValid( enemy ) ) + Highlight_ClearEnemyHighlight( enemy ) + } + ) + + float range = meleeWeapon.GetMeleeLungeTargetRange() + float minDot = AngleToDot( meleeWeapon.GetMeleeLungeTargetAngle() ) + + while( true ) + { + vector viewVector = player.GetViewVector() + vector enemyVector = enemy.GetCenter() - player.EyePosition() + float dist = expect float( enemyVector.Norm() ) + + if ( DotProduct( enemyVector, viewVector ) > minDot && dist < range ) + Highlight_SetEnemyHighlight( enemy, "enemy_sur_base" ) // enemy_sur_base, enemy_sonar, map_scan + else + Highlight_ClearEnemyHighlight( enemy ) + + wait 0.1 + } +} + +#endif // #if SERVER + +bool function AttemptHumanMeleeExecution( entity player, entity syncedTarget, entity meleeWeapon, table traceResult ) +{ + if ( player.PlayerMelee_GetState() == PLAYER_MELEE_STATE_NONE ) + return false + + if ( !IsAlive( player ) ) + return false + + if ( player.IsPhaseShifted() ) + return false + + if ( !CodeCallback_IsValidMeleeExecutionTarget( player, syncedTarget ) ) + return false + + #if SERVER + player.Anim_StopGesture( 0 ) + #endif + + thread PlayerTriesSyncedMelee_FallbackToHumanMeleeAttack( player, syncedTarget, meleeWeapon, traceResult ) + return true +} + +void function PlayerTriesSyncedMelee_FallbackToHumanMeleeAttack( entity player, entity target, entity meleeWeapon, table traceResult ) +{ + if ( !PlayerTriesSyncedMelee( player, target ) ) + { +#if SERVER + HumanMeleeAttack_DoImpact( player, meleeWeapon, traceResult ) +#endif + } +} diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut index 4cfdc6fba..6bc1718a9 100644 --- a/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut @@ -185,13 +185,14 @@ void function NSSendPopUpMessageToPlayer( entity player, string text ) ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.POPUP ) } -void function NSSendAnnouncementMessageToPlayer( entity player, string title, string description, vector color, int priority, int style ) +void function NSSendAnnouncementMessageToPlayer( entity player, string title, string description, vector color, int priority, int style, string image = "" ) { ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.TITLE + " " + title ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + description ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.COLOR + " " + color.x + " " + color.y + " " + color.z ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.PRIORITY + " " + priority ) ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.STYLE + " " + style ) + ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.ASSET + " " + image ) ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.ANNOUNCEMENT ) } @@ -453,6 +454,7 @@ void function AnnouncementMessageHandler_Threaded() Announcement_SetPriority( announcement, client.announcementQueue[0].priority ) Announcement_SetSoundAlias( announcement, SFX_HUD_ANNOUNCE_QUICK ) Announcement_SetStyle( announcement, client.announcementQueue[0].style ) + Announcement_SetIcon( announcement, StringToAsset( strip( client.announcementQueue[0].image ) ) ) AnnouncementFromClass( GetLocalViewPlayer(), announcement ) wait 5 diff --git a/Northstar.Custom/mod/scripts/vscripts/weapons/sh_stim.gnut b/Northstar.Custom/mod/scripts/vscripts/weapons/sh_stim.gnut new file mode 100644 index 000000000..fd8196267 --- /dev/null +++ b/Northstar.Custom/mod/scripts/vscripts/weapons/sh_stim.gnut @@ -0,0 +1,190 @@ + +global function StimShared_Init +global function StimPlayer +global function EndlessStimBegin +global function EndlessStimEnd + + +global int COCKPIT_STIM_FX +global int PILOT_STIM_HLD_FX + +global const float STIM_EFFECT_SEVERITY = 0.4 // assuming 'movement_speedboost_extraScale' is 2.0 + +void function StimShared_Init() +{ + COCKPIT_STIM_FX = PrecacheParticleSystem( $"P_heal" ) + PILOT_STIM_HLD_FX = PrecacheParticleSystem( $"P_pilot_stim_hld" ) + + #if CLIENT + StatusEffect_RegisterEnabledCallback( eStatusEffect.stim_visual_effect, StimVisualsEnabled ) + StatusEffect_RegisterDisabledCallback( eStatusEffect.stim_visual_effect, StimVisualsDisabled ) + #endif + + RegisterSignal( "EndStim" ) + RegisterSignal( "StopEndlessStim" ) +} + +void function EndlessStimBegin( entity player, float effectSeverity ) +{ + StimPlayer_Internal( player, USE_TIME_INFINITE, effectSeverity ) +} +void function EndlessStimEnd( entity player ) +{ + player.Signal( "StopEndlessStim" ) +} + +void function StimPlayer( entity player, float duration, float severity = STIM_EFFECT_SEVERITY ) +{ + StimPlayer_Internal( player, duration, severity ) +} + +void function StimPlayer_Internal( entity player, float duration, float effectSeverity ) +{ + // Handles for tracking status effects + int statusEffectHandle_SpeedBoost = 0 + int statusEffectHandle_StimVFX = 0 + if ( duration == USE_TIME_INFINITE ) + { + statusEffectHandle_SpeedBoost = StatusEffect_AddEndless( player, eStatusEffect.speed_boost, effectSeverity ) + statusEffectHandle_StimVFX = StatusEffect_AddEndless( player, eStatusEffect.stim_visual_effect, 1.0 ) + } + else + { + statusEffectHandle_SpeedBoost = StatusEffect_AddTimed( player, eStatusEffect.speed_boost, effectSeverity, duration + 0.5, 0.25 ) // sound is slightly off + statusEffectHandle_StimVFX = StatusEffect_AddTimed( player, eStatusEffect.stim_visual_effect, 1.0, duration, duration ) + } + +#if SERVER + thread StimThink( player, duration, statusEffectHandle_SpeedBoost, statusEffectHandle_StimVFX ) +#else // CLIENT + entity cockpit = player.GetCockpit() + if ( !IsValid( cockpit ) ) + return + + HealthHUD_ClearFX( player ) +#endif +} + +#if SERVER +void function StimThink( entity player, float duration, int statusEffectHandle_SpeedBoost, int statusEffectHandle_StimVFX ) +{ + player.EndSignal( "OnDeath" ) + player.EndSignal( "OnChangedPlayerClass" ) + player.EndSignal( "player_embarks_titan" ) // Prevent transferring active Stim ability to Titan. + + if ( duration == USE_TIME_INFINITE ) + { + player.EndSignal( "StopEndlessStim" ) + // TF|2 stim loop sounds don't actually loop, use TF|1 loop sound. + // TF|1 sound is very quiet, is there a way to boost its volume (it has 300% volume in TF|1), except playing 3 sounds at once? + // BUG: It still stops playing sometimes for whatever reason ( Too many sounds? ) + EmitSoundOnEntity( player, "Pilot_Stimpack_Loop" ) + } + else + { + EmitSoundOnEntityOnlyToPlayer( player, player, "pilot_stimpack_loop_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "pilot_stimpack_loop_3P" ) + } + + int attachmentIndex = player.LookupAttachment( "CHESTFOCUS" ) + + entity stimFX = StartParticleEffectOnEntity_ReturnEntity( player, PILOT_STIM_HLD_FX, FX_PATTACH_POINT_FOLLOW, attachmentIndex ) + stimFX.SetOwner( player ) + stimFX.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // not owner only + + //thread StimSlowmoAim( player, duration ) + + OnThreadEnd( + function() : ( player, duration, stimFX, statusEffectHandle_SpeedBoost, statusEffectHandle_StimVFX ) + { + if ( !IsValid( player ) ) + return + + if ( IsValid( stimFX ) ) + EffectStop( stimFX ) + + if ( duration == USE_TIME_INFINITE ) + { + StopSoundOnEntity( player, "Pilot_Stimpack_Loop" ) + } + else + { + StopSoundOnEntity( player, "pilot_stimpack_loop_1P" ) + StopSoundOnEntity( player, "pilot_stimpack_loop_3P" ) + } + + if ( statusEffectHandle_SpeedBoost != 0 ) + StatusEffect_Stop( player, statusEffectHandle_SpeedBoost ) + + if ( statusEffectHandle_StimVFX != 0 ) + StatusEffect_Stop( player, statusEffectHandle_StimVFX ) + + player.Signal( "EndStim" ) + } + ) + + if ( duration == USE_TIME_INFINITE ) + WaitForever() + + wait duration - 2.0 + + EmitSoundOnEntityOnlyToPlayer( player, player, "pilot_stimpack_deactivate_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "pilot_stimpack_deactivate_3P" ) + + wait 2.0 +} + +#else // CLIENT +void function StimVisualsEnabled( entity ent, int statusEffect, bool actuallyChanged ) +{ + if ( ent != GetLocalViewPlayer() ) + return + + entity player = ent + + entity cockpit = player.GetCockpit() + if ( !IsValid( cockpit ) ) + return + + int fxHandle = StartParticleEffectOnEntity( cockpit, COCKPIT_STIM_FX, FX_PATTACH_ABSORIGIN_FOLLOW, -1 ) + thread StimScreenFXThink( player, fxHandle, cockpit ) +} + +void function StimVisualsDisabled( entity ent, int statusEffect, bool actuallyChanged ) +{ + if ( ent != GetLocalViewPlayer() ) + return + + ent.Signal( "EndStim" ) +} + +void function StimScreenFXThink( entity player, int fxHandle, entity cockpit ) +{ + player.EndSignal( "EndStim" ) + player.EndSignal( "OnDeath" ) + cockpit.EndSignal( "OnDestroy" ) + + OnThreadEnd( + function() : ( fxHandle ) + { + if ( !EffectDoesExist( fxHandle ) ) + return + + EffectStop( fxHandle, false, true ) + } + ) + + for ( ;; ) + { + float velocityX = Length( player.GetVelocity() ) + + if ( !EffectDoesExist( fxHandle ) ) + break + + velocityX = GraphCapped( velocityX, 0.0, 360, 5, 200 ) + EffectSetControlPointVector( fxHandle, 1, Vector( velocityX, 999, 0 ) ) + WaitFrame() + } +} + +#endif \ No newline at end of file diff --git a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt index 2672dca9c..0ef3ed33b 100644 --- a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt +++ b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt @@ -103,8 +103,11 @@ WeaponData "charge_effect_1p" "wpn_arc_cannon_charge_fp" "charge_effect_3p" "wpn_arc_cannon_charge" "charge_effect_attachment" "muzzle_flash" + "charge_effect2_1p" "wpn_ARC_knob_FP" + "charge_effect2_3p" "wpn_ARC_knob" + "charge_effect2_attachment" "SPINNING_KNOB" - + "charge_effect_show_during_drain" "0" // Spread "spread_stand_hip" "10" diff --git a/Northstar.CustomServers/mod.json b/Northstar.CustomServers/mod.json index d3740a12e..1c08f9bab 100644 --- a/Northstar.CustomServers/mod.json +++ b/Northstar.CustomServers/mod.json @@ -53,6 +53,11 @@ "Name": "ns_progression_enabled", "DefaultValue": "0", "Flags": "ARCHIVE_PLAYERPROFILE" + }, + { + "Name": "ns_allow_team_change", + "DefaultValue": "1", + "Flags": "REPLICATED" } ], "Scripts": [ diff --git a/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut index a20e7aa05..21723ab5d 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut @@ -91,6 +91,8 @@ void function EarnMeterMP_PlayerLifeThink( entity player ) EarnObject pilotReward = PlayerEarnMeter_GetReward( player ) float pilotRewardFrac = PlayerEarnMeter_GetRewardFrac( player ) int lastEarnMeterMode = PlayerEarnMeter_GetMode( player ) + bool saidTitanSoon = false + bool titanReadyMsg = false float lastPassiveGainTime = Time() OnThreadEnd( @@ -148,8 +150,22 @@ void function EarnMeterMP_PlayerLifeThink( entity player ) if ( lastEarnMeterMode == eEarnMeterMode.DEFAULT ) { + if ( Riff_TitanAvailability() != eTitanAvailability.Never ) + { + if ( PlayerEarnMeter_GetOwnedFrac( player ) >= 0.75 && PlayerEarnMeter_GetOwnedFrac( player ) < 0.95 && !saidTitanSoon ) + { + PlayFactionDialogueToPlayer( "mp_titanSoon", player ) + saidTitanSoon = true + } + else if( PlayerEarnMeter_GetOwnedFrac( player ) < 0.75 ) + saidTitanSoon = false + } + if ( PlayerEarnMeter_GetOwnedFrac( player ) < 1.0 ) + { PlayerEarnMeter_DisableGoal( player ) + titanReadyMsg = false + } else if ( player.GetPlayerNetInt( "goalState" ) != eRewardState.UNAVAILABLE ) { // if goal is enabled then the client will show "titan ready" alerts even if it isn't @@ -157,6 +173,11 @@ void function EarnMeterMP_PlayerLifeThink( entity player ) // so unfortunately we have to do this manually player.SetPlayerNetInt( "goalState", eRewardState.AVAILABLE ) PlayerEarnMeter_RefreshGoal( player ) + if( !titanReadyMsg ) + { + Remote_CallFunction_NonReplay( player, "ServerCallback_TitanReadyMessage" ) + titanReadyMsg = true + } } if ( Time() - lastPassiveGainTime > 4.0 && file.passiveMeterGainEnabled ) // this might be 5.0 diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_spawnpoints.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_spawnpoints.gnut deleted file mode 100644 index e69de29bb..000000000 diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut index b77a37b2a..6b4e82d68 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut @@ -18,6 +18,7 @@ global function TrackTitanDamageInPlayerGameStat global function ShouldEntTakeDamage_SPMP global function GetTitanBuildTime global function TitanPlayerHotDropsIntoLevel +global function SetGamemodeAllowsTeamSwitch global function SetRecalculateRespawnAsTitanStartPointCallback @@ -30,10 +31,13 @@ struct { array specCams entity functionref( entity player, entity basePoint ) recalculateRespawnAsTitanStartPointCallback + table playerChangeTeamTimeBuffer + bool gamemodeTeamSwitchEnabled = true } file void function BaseGametype_Init_MPSP() { + AddClientCommandCallback( "changeteam", ClientCommandCallbackChangeTeam ) AddSpawnCallback( "info_intermission", SetIntermissionCamera ) AddPostDamageCallback( "player", AddToTitanDamageStat ) @@ -630,6 +634,51 @@ void function SetRecalculateRespawnAsTitanStartPointCallback( entity functionref file.recalculateRespawnAsTitanStartPointCallback = callbackFunc } +void function SetGamemodeAllowsTeamSwitch( bool enabled ) +{ + file.gamemodeTeamSwitchEnabled = enabled +} + +bool function ClientCommandCallbackChangeTeam( entity player, array args ) +{ + if ( !GetConVarBool( "ns_allow_team_change" ) || IsPrivateMatchSpectator( player ) ) + return true + + if ( !file.gamemodeTeamSwitchEnabled ) + { + SendHudMessage( player, "#TEAMSWITCH_GAMEMODE", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + return true + } + + if ( !( player in file.playerChangeTeamTimeBuffer ) ) + { + file.playerChangeTeamTimeBuffer[ player ] <- Time() + 5.0 + } + else + { + if ( file.playerChangeTeamTimeBuffer[ player ] > Time() ) + { + SendHudMessage( player, "#TEAMSWITCH_BUFFER", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + return true + } + } + + if ( player in file.playerChangeTeamTimeBuffer && file.playerChangeTeamTimeBuffer[ player ] < Time() ) + file.playerChangeTeamTimeBuffer[ player ] = Time() + 5.0 + + if ( !GamePlaying() ) + { + SendHudMessage( player, "#TEAMSWITCH_GAMEPLAY", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + return true + } + if ( GetCurrentPlaylistVarInt( "max_teams", 0 ) > 1 && !IsFFAGame() ) + SetTeam( player, GetOtherTeam( player.GetTeam() ) ) + else + SendHudMessage( player, "#TEAMSWITCH_DISABLED", -1, 0.4, 255, 255, 255, 255, 0.15, 3.0, 0.5 ) + + return true +} + // stuff to change later bool function ShouldEntTakeDamage_SPMP( entity ent, var damageInfo ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut index 0c66f5a96..17323c38b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut @@ -226,37 +226,7 @@ void function SetWinner( int team, string winningReason = "", string losingReaso SetGameState( eGameState.WinnerDetermined ) ScoreEvent_MatchComplete( team ) - array players = GetPlayerArray() - int functionref( entity, entity ) compareFunc = GameMode_GetScoreCompareFunc( GAMETYPE ) - if ( compareFunc != null ) - { - players.sort( compareFunc ) - int playerCount = players.len() - int currentPlace = 1 - for ( int i = 0; i < 3; i++ ) - { - if ( i >= playerCount ) - continue - - if ( i > 0 && compareFunc( players[i - 1], players[i] ) != 0 ) - currentPlace += 1 - - switch( currentPlace ) - { - case 1: - UpdatePlayerStat( players[i], "game_stats", "mvp" ) - UpdatePlayerStat( players[i], "game_stats", "mvp_total" ) - UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) - break - case 2: - UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) - break - case 3: - UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) - break - } - } - } + RegisterMatchStats_OnMatchComplete() } } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut index 74a9088b8..84b09ec8d 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut @@ -13,6 +13,7 @@ global function UpdateTitanCoreEarnedStat global function PreScoreEventUpdateStats global function PostScoreEventUpdateStats global function Stats_OnPlayerDidDamage +global function RegisterMatchStats_OnMatchComplete struct { table< string, array > refs @@ -36,7 +37,6 @@ void function Stats_Init() AddCallback_OnPlayerRespawned( OnPlayerRespawned ) AddCallback_OnClientConnected( OnClientConnected ) AddCallback_OnClientDisconnected( OnClientDisconnected ) - AddCallback_GameStateEnter( eGameState.WinnerDetermined, OnWinnerDetermined ) thread HandleDistanceAndTimeStats_Threaded() thread SaveStatsPeriodically_Threaded() @@ -813,7 +813,7 @@ void function OnPlayerRespawned( entity player ) thread SetLastPosForDistanceStatValid_Threaded( player, true ) } -void function OnWinnerDetermined() +void function RegisterMatchStats_OnMatchComplete() { // award players for match completed, wins, and losses foreach ( entity player in GetPlayerArray() ) @@ -888,30 +888,28 @@ void function OnWinnerDetermined() player.SetPersistentVar( "kdratio_lifetime_pvp", kdratio_lifetimepvp ) } - // award mvp and top 3 in each team - if ( !IsFFAGame() ) + array players = GetPlayerArray() + players.sort( GetScoreboardCompareFunc() ) + int playerCount = players.len() + int currentPlace = 1 + for ( int i = 0; i < 3; i++ ) { - string gamemode = GameRules_GetGameMode() - int functionref( entity, entity ) compareFunc = GameMode_GetScoreCompareFunc( gamemode ) - - for( int team = 0; team < MAX_TEAMS; team++ ) + if ( i >= playerCount ) + continue + + int functionref( entity, entity ) compareFunc = GetScoreboardCompareFunc() + if ( i > 0 && compareFunc( players[i - 1], players[i] ) != 0 ) + currentPlace += 1 + switch( currentPlace ) { - array players = GetPlayerArrayOfTeam( team ) - if ( compareFunc == null ) - { - printt( "gamemode doesn't have a compare func to get the top 3" ) - return - } - players.sort( compareFunc ) - int maxAwards = int( min( players.len(), 3 ) ) - for ( int i = 0; i < maxAwards; i++ ) - { - if ( i == 0 ) - Stats_IncrementStat( players[ i ], "game_stats", "mvp", "", 1.0 ) - Stats_IncrementStat( players[ i ], "game_stats", "top3OnTeam", "", 1.0 ) - } + case 1: // MVP have two parallel stats which one registers MVP for the map played and the other goes to the player's stats menu as a total MVP times + UpdatePlayerStat( players[i], "game_stats", "mvp" ) + UpdatePlayerStat( players[i], "game_stats", "mvp_total" ) + case 2: + case 3: + UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) // Ingame this is the "Times Top 3" for the whole match, not per team + break } - } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts.nut b/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts.nut index 7a7498b8c..4bf195b6d 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_loadouts.nut @@ -1509,7 +1509,7 @@ string function GetValidatedPersistentLoadoutValue( entity player, string loadou { printt( "Invalid Loadout Property: ", loadoutType, loadoutIndex, loadoutProperty, value ) value = ResetLoadoutPropertyToDefault( player, loadoutType, loadoutIndex, loadoutProperty ) //TODO: This will call player.SetPersistentVar() directly. Awkward to do this in a getter function - ClientCommand( player, "disconnect #RESETTING_LOADOUT", 0 ) //Kick player out with a "Resetting Invalid Loadout" message. Mainly necessary so UI/Client script don't crash out later with known, bad data from persistence + NSDisconnectPlayer( player, "#RESETTING_LOADOUT" ) // Kick player out with a "Resetting Invalid Loadout" message. Mainly necessary so UI/Client script don't crash out later with known, bad data from persistence } } @@ -1519,7 +1519,8 @@ string function GetValidatedPersistentLoadoutValue( entity player, string loadou { printt( "Invalid Loadout Property: ", loadoutType, loadoutIndex, loadoutProperty, value ) value = ResetLoadoutPropertyToDefault( player, loadoutType, loadoutIndex, loadoutProperty ) //TODO: This will call player.SetPersistentVar() directly. Awkward to do this in a getter function - ClientCommand( player, "disconnect #RESETTING_LOADOUT", 0 ) //Kick player out with a "Resetting Invalid Loadout" message. Mainly necessary so UI/Client script don't crash out later with known, bad data from persistence + NSDisconnectPlayer( player, "#RESETTING_LOADOUT" ) // Kick player out with a "Resetting Invalid Loadout" message. Mainly necessary so UI/Client script don't crash out later with known, bad data from persistence + } ValidateSkinAndCamoIndexesAsAPair( player, loadoutType, loadoutIndex, loadoutProperty, value ) //TODO: This is awkward, has the potential to call a SetPersistentLoadoutValue() if skinIndex and camoIndex are not correct as a pair diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_progression.nut b/Northstar.CustomServers/mod/scripts/vscripts/sh_progression.nut index 3297643ec..307548d7b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/sh_progression.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_progression.nut @@ -169,8 +169,13 @@ void function UpdateCachedLoadouts_Threaded() // below here is just making all the menu models update properly and such #if UI - uiGlobal.pilotSpawnLoadoutIndex = GetPersistentSpawnLoadoutIndex( GetUIPlayer(), "pilot" ) - uiGlobal.titanSpawnLoadoutIndex = GetPersistentSpawnLoadoutIndex( GetUIPlayer(), "titan" ) + entity UIPlayer = GetUIPlayer() + + if ( !IsValid( UIPlayer ) ) + return + + uiGlobal.pilotSpawnLoadoutIndex = GetPersistentSpawnLoadoutIndex( UIPlayer, "pilot" ) + uiGlobal.titanSpawnLoadoutIndex = GetPersistentSpawnLoadoutIndex( UIPlayer, "titan" ) #endif #if CLIENT diff --git a/README.md b/README.md index 4dbbd6edd..3d7a9019c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Translation status -[Squirrel](http://www.squirrel-lang.org/squirreldoc/reference/index.html) scripts used to recreate server-side gamelogic and add [custom content](https://r2northstar.gitbook.io/r2northstar-wiki/using-northstar/gamemodes) to the game. +[Squirrel](http://www.squirrel-lang.org/squirreldoc/reference/index.html) scripts used to recreate server-side gamelogic and add [custom content](https://docs.northstar.tf/Wiki/using-northstar/gamemodes/) to the game. ## Contents: