diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..cc83d7cd3 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,9 @@ +# Add 'needs code review' label to any changes within the entire repository +needs code review: +- changed-files: + - any-glob-to-any-file: '**' + +# Add 'needs testing' label to any changes within the entire repository +needs testing: +- changed-files: + - any-glob-to-any-file: '**' diff --git a/.github/nativefuncs.json b/.github/nativefuncs.json index 889432d73..110429031 100644 --- a/.github/nativefuncs.json +++ b/.github/nativefuncs.json @@ -18,6 +18,12 @@ "returnTypeString":"void", "argTypes":"string modName, bool enabled" }, + { + "name":"NSIsModRemote", + "helpText":"", + "returnTypeString":"bool", + "argTypes":"string modName" + }, { "name":"NSGetModDescriptionByModName", "helpText":"", @@ -266,6 +272,12 @@ "returnTypeString":"void", "argTypes":"string modName, bool enabled" }, + { + "name":"NSIsModRemote", + "helpText":"", + "returnTypeString":"bool", + "argTypes":"string modName" + }, { "name":"NSGetModDescriptionByModName", "helpText":"", @@ -466,6 +478,12 @@ "returnTypeString":"void", "argTypes":"string modName, bool enabled" }, + { + "name":"NSIsModRemote", + "helpText":"", + "returnTypeString":"bool", + "argTypes":"string modName" + }, { "name":"NSGetModDescriptionByModName", "helpText":"", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8cda06a32..e4dd3515d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,3 +16,11 @@ Note that commit messages in PRs will generally be squashed to keep commit histo --> Replace this line with a description of your change (and screenshots/screenrecordings if applicable). + +### Code review: + +Replace this line with anything specific to look out for during code reviews. + +### Testing: + +Replace this line with instructions on how to test your pull request. The more detailed, the easier it is for reviewers to test, the faster your PR gets merged. diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml new file mode 100644 index 000000000..659ff351d --- /dev/null +++ b/.github/workflows/auto-label-pr.yml @@ -0,0 +1,14 @@ +name: Auto-Labeler +on: + pull_request_target: + types: + - opened + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..64b8c0c3c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing +> NOTE: This is the first iteration of this file. You're welcome to pull request changes + +### Contents +- [Making issues](#Making-issues) +- [Making pull requests](#Making-pull-requests) +- [Formatting code](#Formatting-code) + +## Making issues +When creating issues, whether to track a bug or suggest a feature, please try to follow this set of rules: +1. When filing a bug report issue, please attach a log file ( Located in `R2Northstar/logs/` ). +2. **Short, consise.** No-one wants to read an essay on why x should be added. +3. When applicable attach a short video / screen shots to better convey what the issue is about. + +## Making pull requests +When creating a pull request please follow this set of rules: +1. **1 Fix/Feature should equal to 1 Pull Request.** The more you do in 1 PR the longer it'll take to merge. +2. Mark your Pull Request as draft if it isnt finished just yet. +3. Properly format your code. As we currently don't have a formatter we're very lax on this. That doesn't mean you don't have to try to format your code. +4. **Mention how to test your changes / add a test mod to make it easier to test** + +## Formatting code +A basic set of rules you should follow when creating a Pull Request + +### Comment your code +- If you're adding a new file you should add a doc comment noting what the file does and its origin + ```cpp + ///----------------------------------------------------------------------------- + /// Origin: Northstar + /// Purpose: handles server-side rui + ///----------------------------------------------------------------------------- + ``` + Alternative to `Origin: Northstar` would be `Origin: Respawn` +- Each function should have a header doc comment + ```cpp + ///----------------------------------------------------------------------------- + /// Sends a string message to player + /// Returns true if it succeeded + ///----------------------------------------------------------------------------- + bool function NSSendInfoMessageToPlayer( entity player, string text ) + ``` +### Functions +- Functions should have spaces in the parentheses + ```cpp + bool function NSSendInfoMessageToPlayer( entity player, string text ) + ``` +- If a function need to be threaded off using `thread` it should have a `_Threaded` suffix + +### File +- Files should use tabs for indentation 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 baceed225..f7c5ee2d1 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt @@ -324,6 +324,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^" @@ -379,8 +391,8 @@ Press Yes if you agree to this. This choice can be changed in the mods menu at a "DOWNLOADING_MOD_TITLE_W_PROGRESS" "Downloading mod (%s1%)" "DOWNLOADING_MOD_TEXT" "Downloading %s1 v%s2..." "DOWNLOADING_MOD_TEXT_W_PROGRESS" "Downloading %s1 v%s2...\n(%s3/%s4 MB)" - "CHECKSUMING_TITLE" "Checksuming mod" - "CHECKSUMING_TEXT" "Verifying contents of %s1 v%s2..." + "CHECKSUMING_TITLE" "Verifying mod integrity" + "CHECKSUMING_TEXT" "Validating files of %s1 v%s2..." "EXTRACTING_MOD_TITLE" "Extracting mod (%s1%)" "EXTRACTING_MOD_TEXT" "Extracting %s1 v%s2...\n(%s3/%s4 MB)" "FAILED_DOWNLOADING" "Failed downloading mod" diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_german.txt b/Northstar.Client/mod/resource/northstar_client_localisation_german.txt index 996a3e2ba..726ad6ac8 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_german.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_german.txt @@ -17,9 +17,9 @@ Drücke Ja, um zuzustimmen. Du kannst diese Entscheidung jederzeit im Modmenü ändern." "BACK_AUTHENTICATION_AGREEMENT" "Authentifizierungs-Einwilligung" "AUTHENTICATION_AGREEMENT" "Authentifizierungs-Einwilligung" - "AUTHENTICATION_AGREEMENT_RESTART" "Ein Neustart ist notwendig, um diese Änderung zu übernehmen" + "AUTHENTICATION_AGREEMENT_RESTART" "Ein Neustart ist notwendig, um diese Änderung zu übernehmen." - "DIALOG_AUTHENTICATING_MASTERSERVER" "Authentifizierung mit Master Server" + "DIALOG_AUTHENTICATING_MASTERSERVER" "Authentifizierung mit Master Server." "AUTHENTICATIONAGREEMENT_NO" "Du hast dich gegen die Authentifizierung mit Northstar entschieden. Du kannst die Authentifizierungs-Einwilligung im Modmenü ansehen." "MENU_TITLE_SERVER_BROWSER" "Server Browser" @@ -320,7 +320,7 @@ Drücke Ja, um zuzustimmen. Du kannst diese Entscheidung jederzeit im Modmenü "SHOW_ONLY_NOT_REQUIRED" "Nur optionale Mods" "SHOW_ONLY_REQUIRED" "Nur notwendige Mods" "PROGRESSION_TOGGLE_DISABLED_HEADER" "Fortschritt aktivieren?" - "TOGGLE_PROGRESSION" "Fortschritt zuschalten" + "TOGGLE_PROGRESSION" "Fortschritt umschalten" "PROGRESSION_TOGGLE_ENABLED_HEADER" "Fortschritt deaktivieren?" "PROGRESSION_TOGGLE_ENABLED_BODY" "Titans, Waffen, Fraktionen, Skins, usw werden freigeschaltet und sind zu jeder Zeit verfügbar .\n\nDies kann in der Mehrspielerlobby zu jedem Zeitpunkt geändert werden." "MATCH_COUNTDOWN_LENGTH" "Countdown für privates Match" @@ -357,7 +357,7 @@ Drücke Ja, um zuzustimmen. Du kannst diese Entscheidung jederzeit im Modmenü "PROGRESSION_DISABLED_HEADER" "Fortschritt deaktiviert!" "WILL_RESET_ALL_SETTINGS" "Dadurch werden ALLE Einstellungen, die zu dieser Kategorie gehören, zurückgesetzt.\n\nDies kann nicht rückgängig gemacht werden." "WILL_RESET_SETTING" "Dies setzten die Einstellungen %s1 auf deren Ursprungeswert zurück.\n\nDies kann nicht rückgängig gemacht werden." - "Y_BUTTON_TOGGLE_PROGRESSION" "%[Y_BUTTON|]% Fortschritt zuschalten." + "Y_BUTTON_TOGGLE_PROGRESSION" "%[Y_BUTTON|]% Fortschritt umschalten" "PROGRESSION_TOGGLE_DISABLED_BODY" "Titans, Waffen, Fraktionen, Skins usw. müssen durch Levelaufstieg freigeschaltet oder mit Verdiensten gekauft werden.\n\nDies kann jederzeit in der Mehrspieler-Lobby geändert werden.\n\n^CC000000Warnung: Wenn Sie derzeit ausgerüstete Gegenstände besitzen, die Sie nicht freigeschaltet haben, werden diese zurückgesetzt!" "PROGRESSION_ENABLED_BODY" "^CCCC0000Fortschritt wurde aktiviert.^\n\nTitans, Waffen, Fraktionen, Skins usw. müssen durch Levelaufstieg freigeschaltet oder mit Verdiensten gekauft werden.\n\nDies kann jederzeit in der Mehrspieler-Lobby geändert werden." "PROGRESSION_DISABLED_BODY" "^CCCC0000Fortschritt wurde deaktiviert.^\n\nTitans, Waffen, Fraktionen, Skins usw. werden alle freigeschaltet und jederzeit nutzbar sein.\n\nDies kann jederzeit in der Mehrspieler-Lobby geändert werden." diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_polish.txt b/Northstar.Client/mod/resource/northstar_client_localisation_polish.txt index 2d94a0411..9b570cebe 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_polish.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_polish.txt @@ -46,7 +46,7 @@ Naciśnij Tak jeżeli wyrażasz zgodę. Wybór może zostać zmieniony w menu Mo // mode settings "MODE_SETTING_CATEGORY_PILOT" "Pilot" "MODE_SETTING_CATEGORY_TITAN" "Tytan" - "MODE_SETTING_CATEGORY_RIFF" "Modyfikatory meczu" + "MODE_SETTING_CATEGORY_RIFF" "Modyfikatory rozgrywki" "MODE_SETTING_CATEGORY_MATCH" "Mecz" "classic_mp" "Klasyczny tryb wieloosobowy" diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt index 91a0dedee..a0f751ff1 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_russian.txt @@ -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" "Выключить" @@ -367,5 +367,12 @@ "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" "Только титаны" } } diff --git a/Northstar.Client/mod/resource/ui/menus/mode_select.menu b/Northstar.Client/mod/resource/ui/menus/mode_select.menu new file mode 100644 index 000000000..bf07164e7 --- /dev/null +++ b/Northstar.Client/mod/resource/ui/menus/mode_select.menu @@ -0,0 +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" + } + + 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/scripts/vscripts/cl_northstar_client_init.nut b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut index 3560fd562..9e683a869 100644 --- a/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut +++ b/Northstar.Client/mod/scripts/vscripts/cl_northstar_client_init.nut @@ -20,6 +20,9 @@ global struct GameStateStruct { int otherHighestScore int maxScore float timeEnd + int serverGameState + int fd_waveNumber + int fd_totalWaves } global struct UIPresenceStruct { 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..5c1a20927 --- /dev/null +++ b/Northstar.Client/mod/scripts/vscripts/mp/levels/cl_mp_glitch.nut @@ -0,0 +1,175 @@ +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 ) +{ + printt( "AddInWorldMinimapObject" ) + printt( screen ) + printt( ent ) + + 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 f17216fbc..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,37 @@ 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 + if ( GetGlobalNetInt( "FD_waveState" ) == WAVE_STATE_INCOMING || GetGlobalNetInt( "FD_waveState" ) == WAVE_STATE_IN_PROGRESS ) + { + gs.fd_waveNumber = GetGlobalNetInt( "FD_currentWave" ) + 1 + gs.fd_totalWaves = GetGlobalNetInt( "FD_totalWaves" ) + } + 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 gs.maxScore = IsRoundBased() ? GetCurrentPlaylistVarInt( "roundscorelimit", 0 ) : GetCurrentPlaylistVarInt( "scorelimit", 0 ) 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 605af3832..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,81 +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 ) + + // 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 ) @@ -99,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_modmenu.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut index 3f643aa3d..f08d69a72 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_ns_modmenu.nut @@ -338,6 +338,10 @@ void function RefreshMods() { string mod = modNames[i] + // Do not display remote mods + if ( NSIsModRemote( mod ) ) + continue + if ( searchTerm.len() && mod.tolower().find( searchTerm ) == null ) continue diff --git a/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut index defb1a56f..e198f7265 100644 --- a/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut +++ b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut @@ -85,28 +85,30 @@ global const ARC_CANNON_BEAM_EFFECT_MOD = $"wpn_arc_cannon_beam_mod" global const ARC_CANNON_FX_TABLE = "exp_arc_cannon" global const ArcCannonTargetClassnames = { - [ "npc_drone" ] = true, - [ "npc_dropship" ] = true, - [ "npc_marvin" ] = true, - [ "npc_prowler" ] = true, - [ "npc_soldier" ] = true, - [ "npc_soldier_heavy" ] = true, - [ "npc_soldier_shield" ] = true, - [ "npc_spectre" ] = true, - [ "npc_stalker" ] = true, - [ "npc_super_spectre" ] = true, - [ "npc_titan" ] = true, - [ "npc_turret_floor" ] = true, - [ "npc_turret_mega" ] = true, - [ "npc_turret_sentry" ] = true, - [ "npc_frag_drone" ] = true, - [ "player" ] = true, - [ "prop_dynamic" ] = true, - [ "prop_script" ] = true, - [ "grenade_frag" ] = true, - [ "rpg_missile" ] = true, - [ "script_mover" ] = true, - [ "turret" ] = true, + [ "npc_drone" ] = true, + [ "npc_dropship" ] = true, + [ "npc_gunship" ] = true, + [ "npc_marvin" ] = true, + [ "npc_prowler" ] = true, + [ "npc_soldier" ] = true, + [ "npc_soldier_heavy" ] = true, + [ "npc_soldier_shield" ] = true, + [ "npc_pilot_elite" ] = true, + [ "npc_spectre" ] = true, + [ "npc_stalker" ] = true, + [ "npc_super_spectre" ] = true, + [ "npc_titan" ] = true, + [ "npc_turret_floor" ] = true, + [ "npc_turret_mega" ] = true, + [ "npc_turret_sentry" ] = true, + [ "npc_frag_drone" ] = true, + [ "player" ] = true, + [ "prop_dynamic" ] = true, + [ "prop_script" ] = true, + [ "grenade_frag" ] = true, + [ "rpg_missile" ] = true, + [ "script_mover" ] = true, + [ "turret" ] = true, } struct { diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut index 97addc241..85b80d744 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut @@ -1,5 +1,4 @@ untyped -// this needs a refactor lol global function CaptureTheFlag_Init global function RateSpawnpoints_CTF @@ -12,16 +11,31 @@ const array SWAP_FLAG_MAPS = [ struct { entity imcFlagSpawn entity imcFlag - entity imcFlagReturnTrigger entity militiaFlagSpawn entity militiaFlag - entity militiaFlagReturnTrigger array imcCaptureAssistList array militiaCaptureAssistList } file + + + + + + + + + +/* + ██████ █████ ██████ ████████ ██ ██ ██████ ███████ ████████ ██ ██ ███████ ███████ ██ █████ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███████ ██████ ██ ██ ██ ██████ █████ ██ ███████ █████ █████ ██ ███████ ██ ███ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ ███████ ██ ███████ ██ ██ ██████ +*/ + void function CaptureTheFlag_Init() { PrecacheModel( CTF_FLAG_MODEL ) @@ -30,30 +44,31 @@ void function CaptureTheFlag_Init() PrecacheParticleSystem( FLAG_FX_ENEMY ) CaptureTheFlagShared_Init() + SetSwitchSidesBased( true ) SetSuddenDeathBased( true ) + SetShouldUseRoundWinningKillReplay( true ) - SetRoundWinningKillReplayKillClasses( false, false ) // make these fully manual + SetRoundWinningKillReplayKillClasses( false, false ) AddCallback_OnClientConnected( CTFInitPlayer ) - + AddCallback_OnClientDisconnected( CTFPlayerDisconnected ) + AddCallback_GameStateEnter( eGameState.Prematch, CreateFlags ) AddCallback_GameStateEnter( eGameState.Epilogue, RemoveFlags ) + AddCallback_GameStateEnter( eGameState.Playing, OnPlaying ) + AddCallback_OnTouchHealthKit( "item_flag", OnFlagCollected ) + AddCallback_OnPlayerKilled( OnPlayerKilled ) AddCallback_OnPilotBecomesTitan( DropFlagForBecomingTitan ) - SetSpawnZoneRatingFunc( DecideSpawnZone_CTF ) AddSpawnpointValidationRule( VerifyCTFSpawnpoint ) - RegisterSignal( "FlagReturnEnded" ) RegisterSignal( "ResetDropTimeout" ) - // setup stuff for the functions in sh_gamemode_ctf - // don't really like using level for stuff but just how it be level.teamFlags <- {} - // setup score event earnmeter values ScoreEvent_SetEarnMeterValues( "KillPilot", 0.05, 0.20 ) ScoreEvent_SetEarnMeterValues( "Headshot", 0.0, 0.02 ) ScoreEvent_SetEarnMeterValues( "FirstStrike", 0.0, 0.05 ) @@ -67,54 +82,21 @@ void function CaptureTheFlag_Init() ScoreEvent_SetEarnMeterValues( "FlagReturn", 0.0, 0.20 ) } -void function RateSpawnpoints_CTF( int checkClass, array spawnpoints, int team, entity player ) -{ - RateSpawnpoints_SpawnZones( checkClass, spawnpoints, team, player ) -} -bool function VerifyCTFSpawnpoint( entity spawnpoint, int team ) -{ - // ensure spawnpoints aren't too close to enemy base - vector allyFlagSpot - vector enemyFlagSpot - foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) ) - { - if( spawn.GetTeam() == team ) - allyFlagSpot = spawn.GetOrigin() - else - enemyFlagSpot = spawn.GetOrigin() - } - - if( Distance2D( spawnpoint.GetOrigin(), allyFlagSpot ) > Distance2D( spawnpoint.GetOrigin(), enemyFlagSpot ) ) - return false - - return true -} -void function CTFInitPlayer( entity player ) -{ - if ( !IsValid( file.imcFlagSpawn ) ) - return - - vector imcSpawn = file.imcFlagSpawn.GetOrigin() - Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_IMC, imcSpawn.x, imcSpawn.y, imcSpawn.z ) - - vector militiaSpawn = file.militiaFlagSpawn.GetOrigin() - Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_MILITIA, militiaSpawn.x, militiaSpawn.y, militiaSpawn.z ) -} -void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) -{ - if ( !IsValid( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ) ) ) // getting a crash idk - return - if ( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ).GetParent() == victim ) - { - if ( victim != attacker && attacker.IsPlayer() ) - AddPlayerScore( attacker, "FlagCarrierKill", victim ) - - DropFlag( victim ) - } -} + + + + + +/* +███████ ██████ █████ ██ ██ ███ ██ ██ ██████ ██████ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ +███████ ██████ ███████ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ + ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ███ ███ ██ ████ ███████ ██████ ██████ ██ ██████ +*/ void function CreateFlags() { @@ -122,25 +104,20 @@ void function CreateFlags() { file.imcFlagSpawn.Destroy() file.imcFlag.Destroy() - file.imcFlagReturnTrigger.Destroy() - + } + if ( IsValid( file.militiaFlagSpawn ) ) + { file.militiaFlagSpawn.Destroy() file.militiaFlag.Destroy() - file.militiaFlagReturnTrigger.Destroy() } foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) ) { - // on some maps flags are on the opposite side from what they should be - // likely this is because respawn uses distance checks from spawns to check this in official - // but i don't like doing that so just using a list of maps to swap them on lol bool switchedSides = HasSwitchedSides() == 1 - - // i dont know why this works and whatever we had before didn't, but yeah + bool shouldSwap = switchedSides - if (!shouldSwap && SWAP_FLAG_MAPS.contains( GetMapName() )) + if ( !shouldSwap && SWAP_FLAG_MAPS.contains( GetMapName() ) ) shouldSwap = !shouldSwap - int flagTeam = spawn.GetTeam() if ( shouldSwap ) @@ -148,48 +125,33 @@ void function CreateFlags() flagTeam = GetOtherTeam( flagTeam ) SetTeam( spawn, flagTeam ) } - - // create flag base + entity base = CreatePropDynamic( CTF_FLAG_BASE_MODEL, spawn.GetOrigin(), spawn.GetAngles(), 0 ) SetTeam( base, spawn.GetTeam() ) svGlobal.flagSpawnPoints[ flagTeam ] = base - // create flag entity flag = CreateEntity( "item_flag" ) flag.SetValueForModelKey( CTF_FLAG_MODEL ) SetTeam( flag, flagTeam ) flag.MarkAsNonMovingAttachment() - flag.Minimap_AlwaysShow( TEAM_IMC, null ) // show flag icon on minimap + flag.Minimap_AlwaysShow( TEAM_IMC, null ) flag.Minimap_AlwaysShow( TEAM_MILITIA, null ) flag.Minimap_SetAlignUpright( true ) DispatchSpawn( flag ) flag.SetModel( CTF_FLAG_MODEL ) - flag.SetOrigin( spawn.GetOrigin() + < 0, 0, base.GetBoundingMaxs().z * 2 > ) // ensure flag doesn't spawn clipped into geometry + flag.SetOrigin( spawn.GetOrigin() + < 0, 0, base.GetBoundingMaxs().z * 2 > ) flag.SetVelocity( < 0, 0, 1 > ) flag.s.canTake <- true - flag.s.playersReturning <- [] level.teamFlags[ flag.GetTeam() ] <- flag - - entity returnTrigger = CreateEntity( "trigger_cylinder" ) - SetTeam( returnTrigger, flagTeam ) - returnTrigger.SetRadius( CTF_GetFlagReturnRadius() ) - returnTrigger.SetAboveHeight( CTF_GetFlagReturnRadius() ) - returnTrigger.SetBelowHeight( CTF_GetFlagReturnRadius() ) - - returnTrigger.SetEnterCallback( OnPlayerEntersFlagReturnTrigger ) - returnTrigger.SetLeaveCallback( OnPlayerExitsFlagReturnTrigger ) - DispatchSpawn( returnTrigger ) - - thread TrackFlagReturnTrigger( flag, returnTrigger ) + thread FlagProximityTracker( flag ) if ( flagTeam == TEAM_IMC ) { file.imcFlagSpawn = base file.imcFlag = flag - file.imcFlagReturnTrigger = returnTrigger SetGlobalNetEnt( "imcFlag", file.imcFlag ) SetGlobalNetEnt( "imcFlagHome", file.imcFlagSpawn ) @@ -198,76 +160,185 @@ void function CreateFlags() { file.militiaFlagSpawn = base file.militiaFlag = flag - file.militiaFlagReturnTrigger = returnTrigger SetGlobalNetEnt( "milFlag", file.militiaFlag ) SetGlobalNetEnt( "milFlagHome", file.militiaFlagSpawn ) } } - // reset the flag states, prevents issues where flag is home but doesnt think it's home when halftime goes SetFlagStateForTeam( TEAM_MILITIA, eFlagState.None ) SetFlagStateForTeam( TEAM_IMC, eFlagState.None ) - - foreach ( entity player in GetPlayerArray() ) - CTFInitPlayer( player ) } void function RemoveFlags() { - // destroy all the flag related things if ( IsValid( file.imcFlagSpawn ) ) { + PlayFX( $"P_phase_shift_main", file.imcFlagSpawn.GetOrigin() ) file.imcFlagSpawn.Destroy() file.imcFlag.Destroy() - file.imcFlagReturnTrigger.Destroy() } + if ( IsValid( file.militiaFlagSpawn ) ) { + PlayFX( $"P_phase_shift_main", file.militiaFlagSpawn.GetOrigin() ) file.militiaFlagSpawn.Destroy() file.militiaFlag.Destroy() - file.militiaFlagReturnTrigger.Destroy() } - - // unsure if this is needed, since the flags are destroyed? idk + SetFlagStateForTeam( TEAM_MILITIA, eFlagState.None ) SetFlagStateForTeam( TEAM_IMC, eFlagState.None ) } -void function TrackFlagReturnTrigger( entity flag, entity returnTrigger ) +void function RateSpawnpoints_CTF( int checkClass, array spawnpoints, int team, entity player ) { - // this is a bit of a hack, it seems parenting the return trigger to the flag actually sets the pickup radius of the flag to be the same as the trigger - // this isn't wanted since only pickups should use that additional radius - flag.EndSignal( "OnDestroy" ) + vector allyFlagSpot + vector enemyFlagSpot + foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) ) + { + if( spawn.GetTeam() == team ) + allyFlagSpot = spawn.GetOrigin() + else + enemyFlagSpot = spawn.GetOrigin() + } + + foreach ( entity spawn in spawnpoints ) + { + float rating = 0.0 + float allyFlagDistance = Distance2D( spawn.GetOrigin(), allyFlagSpot ) + float enemyFlagDistance = Distance2D( spawn.GetOrigin(), enemyFlagSpot ) + + if( enemyFlagDistance > allyFlagDistance ) + { + rating += spawn.NearbyAllyScore( team, "ai" ) + rating += spawn.NearbyAllyScore( team, "titan" ) + rating += spawn.NearbyAllyScore( team, "pilot" ) + + rating += spawn.NearbyEnemyScore( team, "ai" ) + rating += spawn.NearbyEnemyScore( team, "titan" ) + rating += spawn.NearbyEnemyScore( team, "pilot" ) - while ( true ) + rating = rating / allyFlagDistance + } + + if ( spawn == player.p.lastSpawnPoint ) + rating += GetConVarFloat( "spawnpoint_last_spawn_rating" ) + + spawn.CalculateRating( checkClass, team, rating, rating * 0.25 ) + } +} + +bool function VerifyCTFSpawnpoint( entity spawnpoint, int team ) +{ + vector allyFlagSpot + vector enemyFlagSpot + foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) ) { - returnTrigger.SetOrigin( flag.GetOrigin() ) - WaitFrame() + if( spawn.GetTeam() == team ) + allyFlagSpot = spawn.GetOrigin() + else + enemyFlagSpot = spawn.GetOrigin() } + + if( Distance2D( spawnpoint.GetOrigin(), allyFlagSpot ) > Distance2D( spawnpoint.GetOrigin(), enemyFlagSpot ) ) + return false + + return true } -void function SetFlagStateForTeam( int team, int state ) + + + + + + + + +/* +██████ ██ █████ ██ ██ ███████ ██████ ██ ██████ ██████ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██████ ██ ███████ ████ █████ ██████ ██ ██ ██ ██ ███ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███████ ██ ██ ██ ███████ ██ ██ ███████ ██████ ██████ ██ ██████ +*/ + +void function OnPlaying() { - if ( state == eFlagState.Away ) // we tell the client the flag is the player carrying it if they're carrying it - SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", ( team == TEAM_IMC ? file.imcFlag : file.militiaFlag ).GetParent() ) - else - SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", team == TEAM_IMC ? file.imcFlag : file.militiaFlag ) + foreach ( entity player in GetPlayerArray() ) + CTFInitPlayer( player ) +} - SetGlobalNetInt( team == TEAM_IMC ? "imcFlagState" : "milFlagState", state ) +void function CTFInitPlayer( entity player ) +{ + if ( !GamePlaying() ) + return + + if ( IsValid( file.imcFlagSpawn ) ) + { + vector imcSpawn = file.imcFlagSpawn.GetOrigin() + Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_IMC, imcSpawn.x, imcSpawn.y, imcSpawn.z ) + } + + if ( IsValid( file.militiaFlagSpawn ) ) + { + vector militiaSpawn = file.militiaFlagSpawn.GetOrigin() + Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_MILITIA, militiaSpawn.x, militiaSpawn.y, militiaSpawn.z ) + } +} + +void function CTFPlayerDisconnected( entity player ) +{ + // This has no validity checks on the player because the disconnection callback happens in the exact last frame the player entity still exists + if( !GamePlaying() ) + return + + if ( PlayerHasEnemyFlag( player ) ) + DropFlag( player, false ) +} + +void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) +{ + if ( !IsValid( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ) ) ) // getting a crash idk + return + + if ( PlayerHasEnemyFlag( victim ) ) + { + if ( victim != attacker && attacker.IsPlayer() ) + AddPlayerScore( attacker, "FlagCarrierKill", victim ) + + DropFlag( victim ) + } } + + + + + + + + + + +/* +███████ ██ █████ ██████ ██ ██████ ██████ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +█████ ██ ███████ ██ ███ ██ ██ ██ ██ ███ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███████ ██ ██ ██████ ███████ ██████ ██████ ██ ██████ +*/ + bool function OnFlagCollected( entity player, entity flag ) { if ( !IsAlive( player ) || flag.GetParent() != null || player.IsTitan() || player.IsPhaseShifted() ) return false if ( player.GetTeam() != flag.GetTeam() && flag.s.canTake ) - GiveFlag( player, flag ) // pickup enemy flag + GiveFlag( player, flag ) else if ( player.GetTeam() == flag.GetTeam() && IsFlagHome( flag ) && PlayerHasEnemyFlag( player ) ) - CaptureFlag( player, GetFlagForTeam( GetOtherTeam( flag.GetTeam() ) ) ) // cap the flag + CaptureFlag( player, GetFlagForTeam( GetOtherTeam( flag.GetTeam() ) ) ) - return false // don't wanna delete the flag entity + return false // Don't delete the flag } void function GiveFlag( entity player, entity flag ) @@ -276,7 +347,8 @@ void function GiveFlag( entity player, entity flag ) flag.Signal( "ResetDropTimeout" ) flag.SetParent( player, "FLAG" ) - thread DropFlagIfPhased( player, flag ) + if ( GetCurrentPlaylistVarInt( "phase_shift_drop_flag", 0 ) == 1 ) + thread DropFlagIfPhased( player, flag ) // do notifications MessageToPlayer( player, eEventNotifications.YouHaveTheEnemyFlag ) @@ -294,103 +366,10 @@ void function GiveFlag( entity player, entity flag ) SetFlagStateForTeam( flag.GetTeam(), eFlagState.Away ) // used for held } -void function DropFlagIfPhased( entity player, entity flag ) -{ - player.EndSignal( "StartPhaseShift" ) - player.EndSignal( "OnDestroy" ) - - OnThreadEnd( function() : ( player ) - { - if (GetGameState() == eGameState.Playing || GetGameState() == eGameState.SuddenDeath) - DropFlag( player, true ) - }) - // the IsValid check is purely to prevent a crash due to a destroyed flag (epilogue) - while( IsValid(flag) && flag.GetParent() == player ) - WaitFrame() -} - -void function DropFlagForBecomingTitan( entity pilot, entity titan ) -{ - DropFlag( pilot, true ) -} - -void function DropFlag( entity player, bool realDrop = true ) -{ - entity flag = GetFlagForTeam( GetOtherTeam( player.GetTeam() ) ) - - if ( flag.GetParent() != player ) - return - - print( player + " dropped the flag!" ) - - flag.ClearParent() - flag.SetAngles( < 0, 0, 0 > ) - flag.SetVelocity( < 0, 0, 0 > ) - - if ( realDrop ) - { - // start drop timeout countdown - thread TrackFlagDropTimeout( flag ) - - // add to capture assists - if ( player.GetTeam() == TEAM_IMC ) - file.imcCaptureAssistList.append( player ) - else - file.militiaCaptureAssistList.append( player ) - - // do notifications - MessageToPlayer( player, eEventNotifications.YouDroppedTheEnemyFlag ) - EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagDrop" ) - - MessageToTeam( player.GetTeam(), eEventNotifications.PlayerDroppedEnemyFlag, player, player ) - // todo need a sound here maybe - MessageToTeam( GetOtherTeam( player.GetTeam() ), eEventNotifications.PlayerDroppedFriendlyFlag, player, player ) - // todo need a sound here maybe - } - - SetFlagStateForTeam( flag.GetTeam(), eFlagState.Home ) // used for return prompt -} - -void function TrackFlagDropTimeout( entity flag ) -{ - flag.EndSignal( "ResetDropTimeout" ) - - wait CTF_GetDropTimeout() - - ResetFlag( flag ) -} - -void function ResetFlag( entity flag ) -{ - // prevents crash when flag is reset after it's been destroyed due to epilogue - if (!IsValid(flag)) - return - // ensure we can't pickup the flag after it's been dropped but before it's been reset - flag.s.canTake = false - - if ( flag.GetParent() != null ) - DropFlag( flag.GetParent(), false ) - - entity spawn - if ( flag.GetTeam() == TEAM_IMC ) - spawn = file.imcFlagSpawn - else - spawn = file.militiaFlagSpawn - - flag.SetOrigin( spawn.GetOrigin() + < 0, 0, spawn.GetBoundingMaxs().z + 1 > ) - - // we can take it again now - flag.s.canTake = true - - SetFlagStateForTeam( flag.GetTeam(), eFlagState.None ) // used for home - - flag.Signal( "ResetDropTimeout" ) -} - void function CaptureFlag( entity player, entity flag ) { // can only capture flags during normal play or sudden death - if (GetGameState() != eGameState.Playing && GetGameState() != eGameState.SuddenDeath) + if ( GetGameState() != eGameState.Playing && GetGameState() != eGameState.SuddenDeath ) { printt( player + " tried to capture the flag, but the game state was " + GetGameState() + " not " + eGameState.Playing + " or " + eGameState.SuddenDeath) return @@ -415,12 +394,15 @@ void function CaptureFlag( entity player, entity flag ) foreach( entity assistPlayer in assistList ) { - if ( player != assistPlayer ) - AddPlayerScore( assistPlayer, "FlagCaptureAssist", player ) - if( !HasPlayerCompletedMeritScore( assistPlayer ) ) + if ( IsValidPlayer( assistPlayer ) ) { - AddPlayerScore( assistPlayer, "ChallengeCTFCapAssist" ) - SetPlayerChallengeMeritScore( assistPlayer ) + if ( player != assistPlayer ) + AddPlayerScore( assistPlayer, "FlagCaptureAssist", player ) + if( !HasPlayerCompletedMeritScore( assistPlayer ) ) + { + AddPlayerScore( assistPlayer, "ChallengeCTFCapAssist" ) + SetPlayerChallengeMeritScore( assistPlayer ) + } } } @@ -431,10 +413,7 @@ void function CaptureFlag( entity player, entity flag ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_PlayerScore" ) if( !HasPlayerCompletedMeritScore( player ) ) - { - AddPlayerScore( player, "ChallengeCTFRetAssist" ) SetPlayerChallengeMeritScore( player ) - } MessageToTeam( team, eEventNotifications.PlayerCapturedEnemyFlag, player, player ) EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamScore", player.GetTeam(), player ) @@ -442,84 +421,198 @@ void function CaptureFlag( entity player, entity flag ) MessageToTeam( GetOtherTeam( team ), eEventNotifications.PlayerCapturedFriendlyFlag, player, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScores", flag.GetTeam() ) - if ( GameRules_GetTeamScore( team ) == GameMode_GetRoundScoreLimit( GAMETYPE ) - 1 ) + if ( GameRules_GetTeamScore( team ) == GetScoreLimit_FromPlaylist() - 1 ) { PlayFactionDialogueToTeam( "ctf_notifyWin1more", team ) PlayFactionDialogueToTeam( "ctf_notifyLose1more", GetOtherTeam( team ) ) + foreach( entity otherPlayer in GetPlayerArray() ) + Remote_CallFunction_NonReplay( otherPlayer, "ServerCallback_CTF_PlayMatchNearEndMusic" ) } } -void function OnPlayerEntersFlagReturnTrigger( entity trigger, entity player ) +void function DropFlag( entity player, bool realDrop = true ) { - entity flag - if ( trigger.GetTeam() == TEAM_IMC ) - flag = file.imcFlag - else - flag = file.militiaFlag - - if( !IsValid( flag ) || !IsValid( player ) ) - return + entity flag = GetFlagForTeam( GetOtherTeam( player.GetTeam() ) ) - if ( !player.IsPlayer() || player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null ) + if( !IsValid( flag ) || flag.GetParent() != player ) return - thread TryReturnFlag( player, flag ) + print( player + " dropped the flag!" ) + + flag.ClearParent() + flag.SetAngles( < 0, 0, 0 > ) + flag.SetVelocity( < 0, 0, 0 > ) + + if ( realDrop ) + { + if ( player.GetTeam() == TEAM_IMC && !file.imcCaptureAssistList.contains( player ) ) + file.imcCaptureAssistList.append( player ) + + else if( !file.militiaCaptureAssistList.contains( player ) ) + file.militiaCaptureAssistList.append( player ) + + MessageToPlayer( player, eEventNotifications.YouDroppedTheEnemyFlag ) + EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagDrop" ) + + MessageToTeam( player.GetTeam(), eEventNotifications.PlayerDroppedEnemyFlag, player, player ) + MessageToTeam( GetOtherTeam( player.GetTeam() ), eEventNotifications.PlayerDroppedFriendlyFlag, player, player ) + } + + thread TrackFlagDropTimeout( flag ) + SetFlagStateForTeam( flag.GetTeam(), eFlagState.Home ) } -void function OnPlayerExitsFlagReturnTrigger( entity trigger, entity player ) +void function ResetFlag( entity flag ) { - entity flag - if ( trigger.GetTeam() == TEAM_IMC ) - flag = file.imcFlag + flag.s.canTake = false + + if ( flag.GetParent() != null ) + DropFlag( flag.GetParent(), false ) + + entity flagBase + if ( flag.GetTeam() == TEAM_IMC ) + flagBase = file.imcFlagSpawn else - flag = file.militiaFlag + flagBase = file.militiaFlagSpawn - if ( !player.IsPlayer() || player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null ) - return + flag.SetOrigin( flagBase.GetOrigin() + < 0, 0, flagBase.GetBoundingMaxs().z + 1 > ) + + flag.s.canTake = true + + SetFlagStateForTeam( flag.GetTeam(), eFlagState.None ) + + flag.Signal( "ResetDropTimeout" ) +} + +//----------------------------------------------------------------------------- +// Purpose: Check proximity for flag returns +// Input : flag - The flag entity +//----------------------------------------------------------------------------- +void function FlagProximityTracker( entity flag ) +{ + flag.EndSignal( "OnDestroy" ) - player.Signal( "FlagReturnEnded" ) -} + array < entity > playerInsidePerimeter + while( true ) + { + if( !playerInsidePerimeter.len() ) + ArrayRemoveDead( playerInsidePerimeter ) + + foreach ( player in GetPlayerArrayOfTeam_Alive( flag.GetTeam() ) ) + { + if ( Distance( player.GetOrigin(), flag.GetOrigin() ) < CTF_GetFlagReturnRadius() ) + { + if ( player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null ) + continue + + if( playerInsidePerimeter.contains( player ) ) + continue + + playerInsidePerimeter.append( player ) + thread TryReturnFlag( player, flag ) + } + else + { + if( playerInsidePerimeter.contains( player ) ) + { + player.Signal( "CTF_LeftReturnTriggerArea" ) // Cut the progress if outside range + playerInsidePerimeter.removebyvalue( player ) + } + } + } + + WaitFrame() + } +} void function TryReturnFlag( entity player, entity flag ) { - // start return progress bar Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StartReturnFlagProgressBar", Time() + CTF_GetFlagReturnTime() ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagReturnMeter" ) - OnThreadEnd( function() : ( player ) + OnThreadEnd( function() : ( flag, player ) { - // cleanup - Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StopReturnFlagProgressBar" ) - StopSoundOnEntity( player, "UI_CTF_1P_FlagReturnMeter" ) + if ( IsValidPlayer( player ) ) + { + Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StopReturnFlagProgressBar" ) + StopSoundOnEntity( player, "UI_CTF_1P_FlagReturnMeter" ) + } }) - player.EndSignal( "FlagReturnEnded" ) - flag.EndSignal( "FlagReturnEnded" ) // avoid multiple players to return one flag at once + flag.EndSignal( "CTF_ReturnedFlag" ) + flag.EndSignal( "OnDestroy" ) + + player.EndSignal( "CTF_LeftReturnTriggerArea" ) player.EndSignal( "OnDeath" ) + player.EndSignal( "OnDestroy" ) wait CTF_GetFlagReturnTime() - // flag return succeeded - // return flag ResetFlag( flag ) - flag.Signal( "FlagReturnEnded" ) - - // do notifications for return + + MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerReturnedFriendlyFlag, null, player ) + EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_TeamReturnsFlag", flag.GetTeam() ) + PlayFactionDialogueToTeam( "ctf_flagReturnedFriendly", flag.GetTeam() ) + MessageToPlayer( player, eEventNotifications.YouReturnedFriendlyFlag ) AddPlayerScore( player, "FlagReturn", player ) player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, 1 ) - if( !HasPlayerCompletedMeritScore( player ) ) + if ( !HasPlayerCompletedMeritScore( player ) ) { AddPlayerScore( player, "ChallengeCTFRetAssist" ) SetPlayerChallengeMeritScore( player ) } - MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerReturnedFriendlyFlag, null, player ) - EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_TeamReturnsFlag", flag.GetTeam() ) - PlayFactionDialogueToTeam( "ctf_flagReturnedFriendly", flag.GetTeam() ) - MessageToTeam( GetOtherTeam( flag.GetTeam() ), eEventNotifications.PlayerReturnedEnemyFlag, null, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyReturnsFlag", GetOtherTeam( flag.GetTeam() ) ) + EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_ReturnsFlag" ) PlayFactionDialogueToTeam( "ctf_flagReturnedEnemy", GetOtherTeam( flag.GetTeam() ) ) -} \ No newline at end of file + + flag.Signal( "CTF_ReturnedFlag" ) +} + +void function SetFlagStateForTeam( int team, int state ) +{ + if ( state == eFlagState.Away ) // we tell the client the flag is the player carrying it if they're carrying it + SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", ( team == TEAM_IMC ? file.imcFlag : file.militiaFlag ).GetParent() ) + else + SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", team == TEAM_IMC ? file.imcFlag : file.militiaFlag ) + + SetGlobalNetInt( team == TEAM_IMC ? "imcFlagState" : "milFlagState", state ) +} + +void function DropFlagIfPhased( entity player, entity flag ) +{ + player.EndSignal( "StartPhaseShift" ) + player.EndSignal( "OnDestroy" ) + flag.EndSignal( "OnDestroy" ) + + OnThreadEnd( function() : ( player ) + { + if ( IsValidPlayer( player ) ) + { + if ( GetGameState() == eGameState.Playing || GetGameState() == eGameState.SuddenDeath ) + DropFlag( player, true ) + } + }) + + while( flag.GetParent() == player ) + WaitFrame() +} + +void function DropFlagForBecomingTitan( entity pilot, entity titan ) +{ + DropFlag( pilot, true ) +} + +void function TrackFlagDropTimeout( entity flag ) +{ + flag.EndSignal( "CTF_ReturnedFlag" ) + flag.EndSignal( "ResetDropTimeout" ) + flag.EndSignal( "OnDestroy" ) + + wait CTF_GetDropTimeout() + + ResetFlag( flag ) +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut index c91c27d11..fb84cc82b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut @@ -20,12 +20,6 @@ void function GamemodePs_Init() ScoreEvent_SetupEarnMeterValuesForMixedModes() SetTimeoutWinnerDecisionFunc( CheckScoreForDraw ) SetupGenericFFAChallenge() - - // spawnzone stuff - SetShouldCreateMinimapSpawnZones( true ) - - //AddCallback_OnPlayerKilled( CheckSpawnzoneSuspiciousDeaths ) - //AddSpawnCallbackEditorClass( "trigger_multiple", "trigger_mp_spawn_zone", SpawnzoneTriggerInit ) file.militiaPreviousSpawnZones = [ null, null, null ] file.imcPreviousSpawnZones = [ null, null, null ] diff --git a/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut b/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut index 9057f7d8b..3814126ba 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/item_inventory/sv_item_inventory.gnut @@ -32,7 +32,7 @@ void function PrematchClearInventory() // vanilla behavior { foreach( entity player in GetPlayerArray() ) { - PlayerInventory_TakeAllInventoryItems( player ) + thread PlayerInventory_TakeAllInventoryItems( player ) } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut index f7c398d97..0c66f5a96 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut @@ -59,18 +59,21 @@ struct { bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) shouldTryUseProjectileReplayCallback } file -void function SetCallback_TryUseProjectileReplay( bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) callback ) -{ - file.shouldTryUseProjectileReplayCallback = callback -} -bool function ShouldTryUseProjectileReplay( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) -{ - if ( file.shouldTryUseProjectileReplayCallback != null ) - return file.shouldTryUseProjectileReplayCallback( victim, attacker, damageInfo, isRoundEnd ) - // default to true (vanilla behaviour) - return true -} + + + + + + + +/* + ██████ █████ ███ ███ ███████ ███████ ████████ █████ ████████ ███████ ███████ +██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███ ███████ ██ ████ ██ █████ ███████ ██ ███████ ██ █████ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██ ██ ██ ██ ███████ ███████ ██ ██ ██ ██ ███████ ███████ +*/ void function PIN_GameStart() { @@ -101,6 +104,61 @@ void function PIN_GameStart() RegisterSignal( "CleanUpEntitiesForRoundEnd" ) } +void function GameState_EntitiesDidLoad() +{ + if ( GetClassicMPMode() || ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) + ClassicMP_SetupIntro() +} + +void function WaittillGameStateOrHigher( int gameState ) +{ + while ( GetGameState() < gameState ) + svGlobal.levelEnt.WaitSignal( "GameStateChanged" ) +} + +bool function ShouldTryUseProjectileReplay( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) +{ + if ( file.shouldTryUseProjectileReplayCallback != null ) + return file.shouldTryUseProjectileReplayCallback( victim, attacker, damageInfo, isRoundEnd ) + // default to true (vanilla behaviour) + return true +} + +/// This is to move all NPCs that a player owns from one team to the other during a match +/// Auto-Titans, Turrets, Ticks and Hacked Spectres will all move along together with the player to the new Team +/// Also possibly prevents mods that spawns other types of NPCs that players can own from breaking when switching (i.e Drones, Hacked Reapers) +void function OnPlayerChangedTeam( entity player ) +{ + if ( !player.hasConnected ) // Prevents players who just joined to trigger below code, as server always pre setups their teams + return + + if( IsIMCOrMilitiaTeam( player.GetTeam() ) ) + NotifyClientsOfTeamChange( player, GetOtherTeam( player.GetTeam() ), player.GetTeam() ) + + foreach( npc in GetNPCArray() ) + { + entity bossPlayer = npc.GetBossPlayer() + if ( IsValidPlayer( bossPlayer ) && bossPlayer == player && IsAlive( npc ) ) + SetTeam( npc, player.GetTeam() ) + } +} + + + + + + + + + +/* + ██████ █████ ███ ███ ███████ ███████ ███████ ████████ ██ ██ ██████ +██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███ ███████ ██ ████ ██ █████ ███████ █████ ██ ██ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██ ██ ██ ██ ███████ ███████ ███████ ██ ██████ ██ +*/ + void function SetGameState( int newState ) { if ( newState == GetGameState() ) @@ -115,23 +173,166 @@ void function SetGameState( int newState ) callbackFunc() } -void function GameState_EntitiesDidLoad() +void function AddTeamScore( int team, int amount ) { - if ( GetClassicMPMode() || ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) - ClassicMP_SetupIntro() + GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount ) + GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount ) + + int scoreLimit + if ( IsRoundBased() ) + scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE ) + else + scoreLimit = GameMode_GetScoreLimit( GAMETYPE ) + + int score = GameRules_GetTeamScore( team ) + if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath ) + SetWinner( team ) + else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) ) + SetGameState( eGameState.SwitchingSides ) } -void function WaittillGameStateOrHigher( int gameState ) +void function SetWinner( int team, string winningReason = "", string losingReason = "" ) +{ + SetServerVar( "winningTeam", team ) + + file.gameWonThisFrame = true + thread UpdateGameWonThisFrameNextFrame() + + if ( winningReason.len() == 0 ) + file.announceRoundWinnerWinningSubstr = 0 + else + file.announceRoundWinnerWinningSubstr = GetStringID( winningReason ) + + if ( losingReason.len() == 0 ) + file.announceRoundWinnerLosingSubstr = 0 + else + file.announceRoundWinnerLosingSubstr = GetStringID( losingReason ) + + if ( GamePlayingOrSuddenDeath() ) + { + if ( IsRoundBased() ) + { + if ( team != TEAM_UNASSIGNED ) + { + GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 ) + GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 ) + } + + SetGameState( eGameState.WinnerDetermined ) + ScoreEvent_RoundComplete( team ) + } + else + { + 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 + } + } + } + } + } +} + +void function SetTimeoutWinnerDecisionFunc( int functionref() callback ) { - while ( GetGameState() < gameState ) - svGlobal.levelEnt.WaitSignal( "GameStateChanged" ) + file.timeoutWinnerDecisionFunc = callback +} + +void function SetCallback_TryUseProjectileReplay( bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) callback ) +{ + file.shouldTryUseProjectileReplayCallback = callback +} + +void function AddCallback_OnRoundEndCleanup( void functionref() callback ) +{ + file.roundEndCleanupCallbacks.append( callback ) +} + +void function SetShouldUsePickLoadoutScreen( bool shouldUse ) +{ + file.usePickLoadoutScreen = shouldUse +} + +void function SetSwitchSidesBased( bool switchSides ) +{ + file.switchSidesBased = switchSides +} + +void function SetSuddenDeathBased( bool suddenDeathBased ) +{ + file.suddenDeathBased = suddenDeathBased +} + +void function SetTimerBased( bool timerBased ) +{ + file.timerBased = timerBased +} + +void function SetShouldUseRoundWinningKillReplay( bool shouldUse ) +{ + SetServerVar( "roundWinningKillReplayEnabled", shouldUse ) +} + +void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan ) +{ + file.roundWinningKillReplayTrackPilotKills = pilot + file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this +} + +void function SetRoundWinningKillReplayAttacker( entity attacker, int inflictorEHandle = -1 ) +{ + file.roundWinningKillReplayTime = Time() + file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) + file.roundWinningKillReplayAttacker = attacker + file.roundWinningKillReplayInflictorEHandle = inflictorEHandle == -1 ? attacker.GetEncodedEHandle() : inflictorEHandle + file.roundWinningKillReplayTimeOfDeath = Time() } -// logic for individual gamestates: -// eGameState.WaitingForCustomStart + + + + + + +/* + ██████ ██ ██ ███████ ████████ ██████ ███ ███ ███████ ████████ █████ ██████ ████████ +██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ███████ ██ ██ ██ ██ ████ ██ ███████ ██ ███████ ██████ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██████ ███████ ██ ██████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ +*/ + void function GameStateEnter_WaitingForCustomStart() { // unused in release, comments indicate this was supposed to be used for an e3 demo @@ -139,7 +340,22 @@ void function GameStateEnter_WaitingForCustomStart() } -// eGameState.WaitingForPlayers + + + + + + + + +/* +██ ██ █████ ██ ████████ ██ ███ ██ ██████ ███████ ██████ ██████ ██████ ██ █████ ██ ██ ███████ ██████ ███████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ █ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ███ █████ ██ ██ ██████ ██████ ██ ███████ ████ █████ ██████ ███████ +██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███ ███ ██ ██ ██ ██ ██ ██ ████ ██████ ██ ██████ ██ ██ ██ ███████ ██ ██ ██ ███████ ██ ██ ███████ +*/ + void function GameStateEnter_WaitingForPlayers() { foreach ( entity player in GetPlayerArray() ) @@ -171,7 +387,22 @@ void function WaitingForPlayers_ClientConnected( entity player ) ScreenFadeToBlackForever( player, 0.0 ) } -// eGameState.PickLoadout + + + + + + + + +/* +██████ ██ ██████ ██ ██ ██ ██████ █████ ██████ ██████ ██ ██ ████████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██████ ██ ██ █████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██████ ██ ██ ███████ ██████ ██ ██ ██████ ██████ ██████ ██ +*/ + void function GameStateEnter_PickLoadout() { thread GameStateEnter_PickLoadout_Threaded() @@ -190,7 +421,22 @@ void function GameStateEnter_PickLoadout_Threaded() } -// eGameState.Prematch + + + + + + + + +/* +██████ ██████ ███████ ███ ███ █████ ████████ ██████ ██ ██ +██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ +██████ ██████ █████ ██ ████ ██ ███████ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ███████ ██ ██ ██ ██ ██ ██████ ██ ██ +*/ + void function GameStateEnter_Prematch() { int timeLimit = GameMode_GetTimeLimit( GAMETYPE ) * 60 @@ -236,7 +482,22 @@ void function StartGameWithoutClassicMP() } -// eGameState.Playing + + + + + + + + +/* +██████ ██ █████ ██ ██ ██ ███ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ +██████ ██ ███████ ████ ██ ██ ██ ██ ██ ███ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███████ ██ ██ ██ ██ ██ ████ ██████ +*/ + void function GameStateEnter_Playing() { thread GameStateEnter_Playing_Threaded() @@ -279,7 +540,22 @@ void function GameStateEnter_Playing_Threaded() } -// eGameState.WinnerDetermined + + + + + + + + +/* +██ ██ ██ ███ ██ ███ ██ ███████ ██████ ██████ ███████ ████████ ███████ ██████ ███ ███ ██ ███ ██ ███████ ██████ +██ ██ ██ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ██ ██ +██ █ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██████ ██ ██ █████ ██ █████ ██████ ██ ████ ██ ██ ██ ██ ██ █████ ██ ██ +██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███ ███ ██ ██ ████ ██ ████ ███████ ██ ██ ██████ ███████ ██ ███████ ██ ██ ██ ██ ██ ██ ████ ███████ ██████ +*/ + // these are likely innacurate const float ROUND_END_FADE_KILLREPLAY = 1.0 const float ROUND_END_DELAY_KILLREPLAY = 3.0 @@ -446,7 +722,22 @@ void function PlayerWatchesRoundWinningKillReplay( entity player, float replayLe } -// eGameState.SwitchingSides + + + + + + + + +/* +███████ ██ ██ ██ ████████ ██████ ██ ██ ██ ███ ██ ██████ ███████ ██ ██████ ███████ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ █ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ███ ███████ ██ ██ ██ █████ ███████ + ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ███ ███ ██ ██ ██████ ██ ██ ██ ██ ████ ██████ ███████ ██ ██████ ███████ ███████ +*/ + void function GameStateEnter_SwitchingSides() { thread GameStateEnter_SwitchingSides_Threaded() @@ -537,7 +828,22 @@ void function PlayerWatchesSwitchingSidesKillReplay( entity player, bool doRepla } -// eGameState.SuddenDeath + + + + + + + + +/* +███████ ██ ██ ██████ ██████ ███████ ███ ██ ██████ ███████ █████ ████████ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ █████ ███████ ██ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██████ ██████ ██████ ███████ ██ ████ ██████ ███████ ██ ██ ██ ██ ██ +*/ + void function GameStateEnter_SuddenDeath() { // disable respawns, suddendeath calling is done on a kill callback @@ -559,7 +865,22 @@ void function GameStateEnter_SuddenDeath() } -// eGameState.Postmatch + + + + + + + + +/* +██████ ██████ ███████ ████████ ███ ███ █████ ████████ ██████ ██ ██ +██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ +██████ ██ ██ ███████ ██ ██ ████ ██ ███████ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██████ ███████ ██ ██ ██ ██ ██ ██ ██████ ██ ██ +*/ + void function GameStateEnter_Postmatch() { foreach ( entity player in GetPlayerArray() ) @@ -592,7 +913,21 @@ void function ForceFadeToBlack( entity player ) } -// shared across multiple gamestates + + + + + + + + +/* +██ ██ ██ ██ ██ ██████ █████ ██ ██ ██████ █████ ██████ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +█████ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ █████ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ███████ ███████ ██████ ██ ██ ███████ ███████ ██████ ██ ██ ██████ ██ ██ ███████ +*/ void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) { @@ -722,10 +1057,22 @@ void function OnTitanKilled( entity victim, var damageInfo ) } } -void function AddCallback_OnRoundEndCleanup( void functionref() callback ) -{ - file.roundEndCleanupCallbacks.append( callback ) -} + + + + + + + + + +/* +████████ ██████ ██████ ██ ███████ ██ ██ ███ ██ ██████ ████████ ██ ██████ ███ ██ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ████ ██ ██ + ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██ ██████ ██████ ███████ ██ ██████ ██ ████ ██████ ██ ██ ██████ ██ ████ ███████ +*/ void function CleanUpEntitiesForRoundEnd() { @@ -763,120 +1110,6 @@ void function CleanUpEntitiesForRoundEnd() SetPlayerDeathsHidden( false ) } - - -// stuff for gamemodes to call - -void function SetShouldUsePickLoadoutScreen( bool shouldUse ) -{ - file.usePickLoadoutScreen = shouldUse -} - -void function SetSwitchSidesBased( bool switchSides ) -{ - file.switchSidesBased = switchSides -} - -void function SetSuddenDeathBased( bool suddenDeathBased ) -{ - file.suddenDeathBased = suddenDeathBased -} - -void function SetTimerBased( bool timerBased ) -{ - file.timerBased = timerBased -} - -void function SetShouldUseRoundWinningKillReplay( bool shouldUse ) -{ - SetServerVar( "roundWinningKillReplayEnabled", shouldUse ) -} - -void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan ) -{ - file.roundWinningKillReplayTrackPilotKills = pilot - file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this -} - -void function SetRoundWinningKillReplayAttacker( entity attacker, int inflictorEHandle = -1 ) -{ - file.roundWinningKillReplayTime = Time() - file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) - file.roundWinningKillReplayAttacker = attacker - file.roundWinningKillReplayInflictorEHandle = inflictorEHandle == -1 ? attacker.GetEncodedEHandle() : inflictorEHandle - file.roundWinningKillReplayTimeOfDeath = Time() -} - -void function SetWinner( int team, string winningReason = "", string losingReason = "" ) -{ - SetServerVar( "winningTeam", team ) - - file.gameWonThisFrame = true - thread UpdateGameWonThisFrameNextFrame() - - if ( winningReason.len() == 0 ) - file.announceRoundWinnerWinningSubstr = 0 - else - file.announceRoundWinnerWinningSubstr = GetStringID( winningReason ) - - if ( losingReason.len() == 0 ) - file.announceRoundWinnerLosingSubstr = 0 - else - file.announceRoundWinnerLosingSubstr = GetStringID( losingReason ) - - if ( GamePlayingOrSuddenDeath() ) - { - if ( IsRoundBased() ) - { - if ( team != TEAM_UNASSIGNED ) - { - GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 ) - GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 ) - } - - SetGameState( eGameState.WinnerDetermined ) - ScoreEvent_RoundComplete( team ) - } - else - { - 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 - } - } - } - } - } -} - void function UpdateGameWonThisFrameNextFrame() { WaitFrame() @@ -884,29 +1117,6 @@ void function UpdateGameWonThisFrameNextFrame() file.hasKillForGameWonThisFrame = false } -void function AddTeamScore( int team, int amount ) -{ - GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount ) - GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount ) - - int scoreLimit - if ( IsRoundBased() ) - scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE ) - else - scoreLimit = GameMode_GetScoreLimit( GAMETYPE ) - - int score = GameRules_GetTeamScore( team ) - if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath ) - SetWinner( team ) - else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) ) - SetGameState( eGameState.SwitchingSides ) -} - -void function SetTimeoutWinnerDecisionFunc( int functionref() callback ) -{ - file.timeoutWinnerDecisionFunc = callback -} - int function GetWinningTeamWithFFASupport() { if ( !IsFFAGame() ) @@ -936,8 +1146,6 @@ int function GetWinningTeamWithFFASupport() unreachable } -// idk - float function GameState_GetTimeLimitOverride() { return 100 @@ -967,8 +1175,6 @@ float function GetTimeLimit_ForGameMode() return GetCurrentPlaylistVarFloat( playlistString, 10 ) } -// faction dialogue - void function DialoguePlayNormal() { int totalScore = GameMode_GetScoreLimit( GameRules_GetGameMode() ) @@ -1052,22 +1258,4 @@ void function DialoguePlayWinnerDetermined() PlayFactionDialogueToTeam( "scoring_won", winningTeam ) PlayFactionDialogueToTeam( "scoring_lost", losingTeam ) } -} - -/// This is to move all NPCs that a player owns from one team to the other during a match -/// Auto-Titans, Turrets, Ticks and Hacked Spectres will all move along together with the player to the new Team -/// Also possibly prevents mods that spawns other types of NPCs that players can own from breaking when switching (i.e Drones, Hacked Reapers) -void function OnPlayerChangedTeam( entity player ) -{ - if ( !player.hasConnected ) // Prevents players who just joined to trigger below code, as server always pre setups their teams - return - - NotifyClientsOfTeamChange( player, GetOtherTeam( player.GetTeam() ), player.GetTeam() ) - - foreach( npc in GetNPCArray() ) - { - entity bossPlayer = npc.GetBossPlayer() - if ( IsValidPlayer( bossPlayer ) && bossPlayer == player && IsAlive( npc ) ) - SetTeam( npc, player.GetTeam() ) - } -} +} \ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut index 8d859ba63..376c5b7c3 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut @@ -231,6 +231,8 @@ void function OnPrematchStart() void function PlayerWatchesWargamesIntro( entity player ) { + player.EndSignal( "OnDestroy" ) + if ( IsAlive( player ) ) player.Die() @@ -253,8 +255,6 @@ void function PlayerWatchesWargamesIntro( entity player ) // we need to wait a frame if we killed ourselves to spawn into this, so just easier to do it all the time to remove any weirdness WaitFrame() - - player.EndSignal( "OnDestroy" ) player.EndSignal( "OnDeath" ) int factionTeam = ConvertPlayerFactionToIMCOrMilitiaTeam( player ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut index d64e3a5b6..c47552b3e 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut @@ -1,27 +1,30 @@ untyped -global function InitRatings // temp for testing - global function Spawn_Init -global function SetRespawnsEnabled -global function RespawnsEnabled +global function FindSpawnPoint + global function SetSpawnpointGamemodeOverride global function GetSpawnpointGamemodeOverride global function AddSpawnpointValidationRule + +global function SetRespawnsEnabled +global function RespawnsEnabled global function CreateNoSpawnArea global function DeleteNoSpawnArea - -global function FindSpawnPoint +global function SpawnPointInNoSpawnArea global function RateSpawnpoints_Generic global function RateSpawnpoints_Frontline - -global function SetSpawnZoneRatingFunc -global function SetShouldCreateMinimapSpawnZones -global function CreateTeamSpawnZoneEntity global function RateSpawnpoints_SpawnZones global function DecideSpawnZone_Generic -global function DecideSpawnZone_CTF + +global struct spawnZoneProperties{ + int controllingTeam = TEAM_UNASSIGNED + entity minimapEnt = null + float zoneRating = 0.0 +} + +global table< entity, spawnZoneProperties > mapSpawnZones // Global so other scripts can access this for custom ratings if needed struct NoSpawnArea { @@ -35,30 +38,61 @@ struct NoSpawnArea struct { bool respawnsEnabled = true + array noSpawnAreas string spawnpointGamemodeOverride array< bool functionref( entity, int ) > customSpawnpointValidationRules - - table noSpawnAreas + bool shouldCreateMinimapSpawnzones } file + + + + + + + + + + +/* +██████ █████ ███████ ███████ ███████ ██ ██ ███ ██ ██████ ████████ ██ ██████ ███ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ████ ██ ██ +██████ ███████ ███████ █████ █████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██████ ██ ██ ███████ ███████ ██ ██████ ██ ████ ██████ ██ ██ ██████ ██ ████ ███████ +*/ + void function Spawn_Init() -{ +{ + // callbacks for generic spawns AddSpawnCallback( "info_spawnpoint_human", InitSpawnpoint ) - AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint ) AddSpawnCallback( "info_spawnpoint_titan", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_droppod", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_dropship", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint ) AddSpawnCallback( "info_spawnpoint_titan_start", InitSpawnpoint ) - - // callbacks for generic spawns - AddCallback_EntitiesDidLoad( InitPreferSpawnNodes ) + AddSpawnCallback( "info_spawnpoint_droppod_start", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_dropship_start", InitSpawnpoint ) // callbacks for spawnzone spawns AddCallback_GameStateEnter( eGameState.Prematch, ResetSpawnzones ) AddSpawnCallbackEditorClass( "trigger_multiple", "trigger_mp_spawn_zone", AddSpawnZoneTrigger ) -} - -void function InitSpawnpoint( entity spawnpoint ) -{ - spawnpoint.s.lastUsedTime <- -999 + + float friendlyAIValue = 1.75 + if ( GameModeHasCapturePoints() ) + friendlyAIValue = 0.75 + + SpawnPoints_SetRatingMultipliers_Enemy( TD_TITAN, -10.0, -6.0, -1.0 ) + SpawnPoints_SetRatingMultipliers_Enemy( TD_PILOT, -10.0, -6.0, -1.0 ) + SpawnPoints_SetRatingMultipliers_Enemy( TD_AI, -2.0, -0.25, 0.0 ) + + SpawnPoints_SetRatingMultipliers_Friendly( TD_TITAN, 0.25, 1.75, friendlyAIValue ) + SpawnPoints_SetRatingMultipliers_Friendly( TD_PILOT, 0.25, 1.75, friendlyAIValue ) + SpawnPoints_SetRatingMultipliers_Friendly( TD_AI, 0.5, 0.25, 0.0 ) + + SpawnPoints_SetRatingMultiplier_PetTitan( 2.0 ) + + file.shouldCreateMinimapSpawnzones = GetCurrentPlaylistVarInt( "spawn_zone_enabled", 1 ) != 0 } void function SetRespawnsEnabled( bool enabled ) @@ -71,9 +105,10 @@ bool function RespawnsEnabled() return file.respawnsEnabled } -void function AddSpawnpointValidationRule( bool functionref( entity spawn, int team ) rule ) +void function InitSpawnpoint( entity spawnpoint ) { - file.customSpawnpointValidationRules.append( rule ) + spawnpoint.s.lastUsedTime <- -999 + spawnpoint.s.inUse <- false } string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam, vector position, float lifetime, float radius ) @@ -85,11 +120,12 @@ string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam noSpawnArea.lifetime = lifetime noSpawnArea.radius = radius - // generate an id noSpawnArea.id = UniqueString( "noSpawnArea" ) - thread NoSpawnAreaLifetime( noSpawnArea ) + if ( lifetime > 0 ) + thread NoSpawnAreaLifetime( noSpawnArea ) + file.noSpawnAreas.append( noSpawnArea ) return noSpawnArea.id } @@ -101,8 +137,41 @@ void function NoSpawnAreaLifetime( NoSpawnArea noSpawnArea ) void function DeleteNoSpawnArea( string noSpawnIdx ) { - if ( noSpawnIdx in file.noSpawnAreas ) - delete file.noSpawnAreas[ noSpawnIdx ] + foreach ( noSpawnArea in file.noSpawnAreas ) + { + if ( noSpawnArea.id == noSpawnIdx ) + file.noSpawnAreas.removebyvalue( noSpawnArea ) + } +} + +bool function SpawnPointInNoSpawnArea( vector vec, int team ) +{ + foreach ( noSpawnArea in file.noSpawnAreas ) + { + if ( Distance( noSpawnArea.position, vec ) < noSpawnArea.radius ) + { + if ( noSpawnArea.blockedTeam != TEAM_INVALID && noSpawnArea.blockedTeam == team ) + return true + + if ( noSpawnArea.blockOtherTeams != TEAM_INVALID && noSpawnArea.blockOtherTeams != team ) + return true + } + } + + return false +} + +bool function IsSpawnpointValidDrop( entity spawnpoint, int team ) +{ + if ( spawnpoint.IsOccupied() || spawnpoint.s.inUse ) + return false + + return true +} + +void function AddSpawnpointValidationRule( bool functionref( entity spawn, int team ) rule ) +{ + file.customSpawnpointValidationRules.append( rule ) } void function SetSpawnpointGamemodeOverride( string gamemode ) @@ -114,35 +183,38 @@ string function GetSpawnpointGamemodeOverride() { if ( file.spawnpointGamemodeOverride != "" ) return file.spawnpointGamemodeOverride - else - return GAMETYPE - unreachable + return GAMETYPE } -void function InitRatings( entity player, int team ) -{ - if ( player != null ) - SpawnPoints_InitRatings( player, team ) // no idea what the second arg supposed to be lol -} + + + + + + + + + +/* +███████ ██████ █████ ██ ██ ███ ██ ██████ ██████ ██████ ███████ ██████ ██ ███ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ +███████ ██████ ███████ ██ █ ██ ██ ██ ██ ██ ██ ██████ ██ ██ █████ ██████ ██ ██ ██ ██ ██ ███ + ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ███ ███ ██ ████ ██████ ██ ██ ██████ ███████ ██ ██ ██ ██ ████ ██████ +*/ entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnpoint ) { int team = player.GetTeam() - if ( HasSwitchedSides() ) - team = ( team == TEAM_MILITIA ) ? TEAM_IMC : TEAM_MILITIA - + array spawnpoints if ( useStartSpawnpoint ) spawnpoints = isTitan ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetPilotStart( team ) else spawnpoints = isTitan ? SpawnPoints_GetTitan() : SpawnPoints_GetPilot() - InitRatings( player, player.GetTeam() ) - - // don't think this is necessary since we call discardratings - //foreach ( entity spawnpoint in spawnpoints ) - // spawnpoint.CalculateRating( isTitan ? TD_TITAN : TD_PILOT, team, 0.0, 0.0 ) + SpawnPoints_InitRatings( player, team ) void functionref( int, array, int, entity ) ratingFunc = isTitan ? GameMode_GetTitanSpawnpointsRatingFunc( GAMETYPE ) : GameMode_GetPilotSpawnpointsRatingFunc( GAMETYPE ) ratingFunc( isTitan ? TD_TITAN : TD_PILOT, spawnpoints, team, player ) @@ -166,36 +238,46 @@ entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnp spawnpoints = useStartSpawnpoint ? SpawnPoints_GetPilotStart( team ) : SpawnPoints_GetPilot() } - entity spawnpoint = GetBestSpawnpoint( player, spawnpoints ) + entity spawnpoint = GetBestSpawnpoint( player, spawnpoints, isTitan ) spawnpoint.s.lastUsedTime = Time() player.SetLastSpawnPoint( spawnpoint ) + + //SpawnPoints_DiscardRatings() return spawnpoint } -entity function GetBestSpawnpoint( entity player, array spawnpoints ) +entity function GetBestSpawnpoint( entity player, array spawnpoints, bool isTitan ) { - // not really 100% sure on this randomisation, needs some thought array validSpawns + + // I know this looks hacky but the native funcs to get the spawns is returning null arrays for FFA idk why. + if ( IsFFAGame() ) + { + spawnpoints.clear() + if ( isTitan ) + spawnpoints = GetEntArrayByClass_Expensive( "info_spawnpoint_titan" ) + else + spawnpoints = GetEntArrayByClass_Expensive( "info_spawnpoint_human" ) + } + foreach ( entity spawnpoint in spawnpoints ) { if ( IsSpawnpointValid( spawnpoint, player.GetTeam() ) ) validSpawns.append( spawnpoint ) } - if ( !validSpawns.len() ) + if ( !validSpawns.len() ) // First validity check { - // no valid spawns, very bad, so dont care about spawns being valid anymore - print( "found no valid spawns! spawns may be subpar!" ) + CodeWarning( "Map has no valid spawn points for " + GAMETYPE + " gamemode, attempting any other possible spawn point" ) foreach ( entity spawnpoint in spawnpoints ) validSpawns.append( spawnpoint ) } - // last resort - if ( !validSpawns.len() ) + if ( !validSpawns.len() ) // On all validity check, just gather the most basic spawn { - print( "map has literally 0 spawnpoints, as such everything is fucked probably, attempting to use info_player_start if present" ) + CodeWarning( "Map has no proper spawn points, falling back to info_player_start" ) entity start = GetEnt( "info_player_start" ) if ( IsValid( start ) ) @@ -203,14 +285,19 @@ entity function GetBestSpawnpoint( entity player, array spawnpoints ) start.s.lastUsedTime <- -999 validSpawns.append( start ) } + else + throw( "Map has no player spawns at all" ) } - return validSpawns.getrandom() // slightly randomize it + if ( IsFFAGame() ) + return validSpawns.getrandom() + + return validSpawns[0] // Return first entry in the array because native have already sorted everything through the ratings, so first one is the best one } bool function IsSpawnpointValid( entity spawnpoint, int team ) { - if ( !spawnpoint.HasKey( "ignoreGamemode" ) || ( spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) ) // used by script-spawned spawnpoints + if ( !spawnpoint.HasKey( "ignoreGamemode" ) || spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) // used by script-spawned spawnpoints { if ( file.spawnpointGamemodeOverride != "" ) { @@ -222,223 +309,149 @@ bool function IsSpawnpointValid( entity spawnpoint, int team ) return false } - if( IsFFAGame() && !spawnpoint.IsVisibleToEnemies( team ) ) - return true - - int compareTeam = spawnpoint.GetTeam() - if ( HasSwitchedSides() ) - compareTeam = ( compareTeam == TEAM_MILITIA ) ? TEAM_IMC : TEAM_MILITIA - foreach ( bool functionref( entity, int ) customValidationRule in file.customSpawnpointValidationRules ) if ( !customValidationRule( spawnpoint, team ) ) return false - if ( spawnpoint.GetTeam() > 0 && compareTeam != team ) + if ( !IsSpawnpointValidDrop( spawnpoint, team ) || Time() - spawnpoint.s.lastUsedTime <= 10.0 ) return false - if ( spawnpoint.IsOccupied() ) + if ( SpawnPointInNoSpawnArea( spawnpoint.GetOrigin(), team ) ) return false - - if ( Time() - spawnpoint.s.lastUsedTime <= 10.0 ) - return false - - foreach ( k, NoSpawnArea noSpawnArea in file.noSpawnAreas ) + + // Line of Sight Check, could use IsVisibleToEnemies but apparently that considers only players, not NPCs + array< entity > enemyTitans = GetTitanArrayOfEnemies( team ) + if ( GetConVarBool( "spawnpoint_avoid_npc_titan_sight" ) ) { - if ( Distance( noSpawnArea.position, spawnpoint.GetOrigin() ) > noSpawnArea.radius ) - continue - - if ( noSpawnArea.blockedTeam != TEAM_INVALID && noSpawnArea.blockedTeam == team ) - return false - - if ( noSpawnArea.blockOtherTeams != TEAM_INVALID && noSpawnArea.blockOtherTeams != team ) - return false + foreach ( titan in enemyTitans ) + { + if ( IsAlive( titan ) && titan.IsNPC() && titan.CanSee( spawnpoint ) ) + return false + } } - - const minEnemyDist = 1200.0 - array< entity > spawnBlockers = GetPlayerArrayEx( "any", TEAM_ANY, spawnpoint.GetTeam(), spawnpoint.GetOrigin(), minEnemyDist ) - spawnBlockers.extend( GetProjectileArrayEx( "any", TEAM_ANY, spawnpoint.GetTeam(), spawnpoint.GetOrigin(), minEnemyDist ) ) - spawnBlockers.extend( GetNPCArrayEx( "any", TEAM_ANY, spawnpoint.GetTeam(), spawnpoint.GetOrigin(), minEnemyDist ) ) - if ( spawnBlockers.len() ) - return false - // los check return !spawnpoint.IsVisibleToEnemies( team ) } -// SPAWNPOINT RATING FUNCS BELOW -// generic -struct { - array preferSpawnNodes -} spawnStateGeneric -void function RateSpawnpoints_Generic( int checkClass, array spawnpoints, int team, entity player ) -{ - if ( !IsFFAGame() ) - { - // use frontline spawns in 2-team modes - RateSpawnpoints_Frontline( checkClass, spawnpoints, team, player ) - return - } - else - { - // todo: ffa spawns :terror: - } - // old algo: keeping until we have a better ffa spawn algo - // i'm not a fan of this func, but i really don't have a better way to do this rn, and it's surprisingly good with los checks implemented now - - // calculate ratings for preferred nodes - // this tries to prefer nodes with more teammates, then activity on them - // todo: in the future it might be good to have this prefer nodes with enemies up to a limit of some sort - // especially in ffa modes i could deffo see this falling apart a bit rn - // perhaps dead players could be used to calculate some sort of activity rating? so high-activity points with an even balance of friendly/unfriendly players are preferred - array preferSpawnNodeRatings - foreach ( vector preferSpawnNode in spawnStateGeneric.preferSpawnNodes ) + + + + +/* +██████ ██████ ██ ███ ██ ████████ ██████ █████ ████████ ██ ███ ██ ██████ +██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ +██████ ██ ██ ██ ██ ██ ██ ██ ██████ ███████ ██ ██ ██ ██ ██ ██ ███ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██████ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██████ +*/ + +void function RateSpawnpoints_Generic( int checkClass, array spawnpoints, int team, entity player ) +{ + foreach ( entity spawnpoint in spawnpoints ) { - float currentRating + float currentRating = 0.0 - // this seems weird, not using rn - //Frontline currentFrontline = GetCurrentFrontline( team ) - //if ( !IsFFAGame() || currentFrontline.friendlyCenter != < 0, 0, 0 > ) - // currentRating += max( 0.0, ( 1000.0 - Distance2D( currentFrontline.origin, preferSpawnNode ) ) / 200 ) + // Gather friendly scoring first to give positive rating first + currentRating += spawnpoint.NearbyAllyScore( team, "ai" ) + currentRating += spawnpoint.NearbyAllyScore( team, "titan" ) + currentRating += spawnpoint.NearbyAllyScore( team, "pilot" ) - foreach ( entity nodePlayer in GetPlayerArray() ) - { - float currentChange = 0.0 - - // the closer a player is to a node the more they matter - float dist = Distance2D( preferSpawnNode, nodePlayer.GetOrigin() ) - if ( dist > 600.0 ) - continue - - currentChange = ( 600.0 - dist ) / 5 - if ( player == nodePlayer ) - currentChange *= -3 // always try to stay away from places we've already spawned - else if ( !IsAlive( nodePlayer ) ) // dead players mean activity which is good, but they're also dead so they don't matter as much as living ones - currentChange *= 0.6 - if ( nodePlayer.GetTeam() != player.GetTeam() ) // if someone isn't on our team and alive they're probably bad - { - if ( IsFFAGame() ) // in ffa everyone is on different teams, so this isn't such a big deal - currentChange *= -0.2 - else - currentChange *= -0.6 - } - - currentRating += currentChange - } + // Enemies then subtract that rating ( Values already returns negative, so no need to apply subtract again ) + currentRating += spawnpoint.NearbyEnemyScore( team, "ai" ) + currentRating += spawnpoint.NearbyEnemyScore( team, "titan" ) + currentRating += spawnpoint.NearbyEnemyScore( team, "pilot" ) - preferSpawnNodeRatings.append( currentRating ) - } - - foreach ( entity spawnpoint in spawnpoints ) - { - float currentRating - float petTitanModifier - // scale how much a given spawnpoint matters to us based on how far it is from each node - bool spawnHasRecievedInitialBonus = false - for ( int i = 0; i < spawnStateGeneric.preferSpawnNodes.len(); i++ ) - { - // bonus if autotitan is nearish - if ( IsAlive( player.GetPetTitan() ) && Distance( player.GetPetTitan().GetOrigin(), spawnStateGeneric.preferSpawnNodes[ i ] ) < 1200.0 ) - petTitanModifier += 10.0 - - float dist = Distance2D( spawnpoint.GetOrigin(), spawnStateGeneric.preferSpawnNodes[ i ] ) - if ( dist > 750.0 ) - continue - - if ( dist < 600.0 && !spawnHasRecievedInitialBonus ) - { - currentRating += 10.0 - spawnHasRecievedInitialBonus = true // should only get a bonus for simply being by a node once to avoid over-rating - } + if ( spawnpoint == player.p.lastSpawnPoint ) // Reduce the rating of the spawn point used previously + currentRating += GetConVarFloat( "spawnpoint_last_spawn_rating" ) - currentRating += ( preferSpawnNodeRatings[ i ] * ( ( 750.0 - dist ) / 75 ) ) + max( RandomFloat( 1.25 ), 0.9 ) - if ( dist < 250.0 ) // shouldn't get TOO close to an active node - currentRating *= 0.7 - - if ( spawnpoint.s.lastUsedTime < 10.0 ) - currentRating *= 0.7 - } - - float rating = spawnpoint.CalculateRating( checkClass, team, currentRating, currentRating + petTitanModifier ) - //print( "spawnpoint at " + spawnpoint.GetOrigin() + " has rating: " + ) - - if ( rating != 0.0 || currentRating != 0.0 ) - print( "rating = " + rating + ", internal rating = " + currentRating ) + spawnpoint.CalculateRating( checkClass, team, currentRating, currentRating * 0.25 ) } } -void function InitPreferSpawnNodes() +void function RateSpawnpoints_Frontline( int checkClass, array spawnpoints, int team, entity player ) { - foreach ( entity hardpoint in GetEntArrayByClass_Expensive( "info_hardpoint" ) ) + Frontline currentFrontline = GetFrontline( team ) + + vector inverseFrontlineDir = currentFrontline.combatDir * -1 + vector adjustedPosition = currentFrontline.origin + currentFrontline.combatDir * 8000 + + SpawnPoints_InitFrontlineData( adjustedPosition, currentFrontline.combatDir, currentFrontline.origin, currentFrontline.friendlyCenter, 4000 ) + + foreach ( entity spawnpoint in spawnpoints ) { - if ( !hardpoint.HasKey( "hardpointGroup" ) ) - continue - - if ( hardpoint.kv.hardpointGroup != "A" && hardpoint.kv.hardpointGroup != "B" && hardpoint.kv.hardpointGroup != "C" ) - continue - - spawnStateGeneric.preferSpawnNodes.append( hardpoint.GetOrigin() ) + float frontlineRating = spawnpoint.CalculateFrontlineRating() + + spawnpoint.CalculateRating( checkClass, team, frontlineRating, frontlineRating * 0.25 ) } - - //foreach ( entity frontline in GetEntArrayByClass_Expensive( "info_frontline" ) ) - // spawnStateGeneric.preferSpawnNodes.append( frontline.GetOrigin() ) } -// frontline -void function RateSpawnpoints_Frontline( int checkClass, array spawnpoints, int team, entity player ) -{ - float rating = RandomFloatRange( 0.0, 100.0 ) - foreach ( entity spawnpoint in spawnpoints ) - spawnpoint.CalculateRating( checkClass, player.GetTeam(), rating, rating ) -} -// spawnzones -struct { - array mapSpawnzoneTriggers - entity functionref( array, int ) spawnzoneRatingFunc - bool shouldCreateMinimapSpawnzones = false - - // for DecideSpawnZone_Generic - table activeTeamSpawnzones - table activeTeamSpawnzoneMinimapEnts -} spawnStateSpawnzones + + + + + + + + +/* +███████ ██████ █████ ██ ██ ███ ██ ███████ ██████ ███ ██ ███████ ███████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ███ ██ ██ ████ ██ ██ ██ +███████ ██████ ███████ ██ █ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ █████ ███████ + ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ███ ███ ██ ████ ███████ ██████ ██ ████ ███████ ███████ +*/ void function ResetSpawnzones() { - spawnStateSpawnzones.activeTeamSpawnzones.clear() - - foreach ( int team, entity minimapEnt in spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts ) - if ( IsValid( minimapEnt ) ) - minimapEnt.Destroy() - - spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts.clear() + foreach ( zone, zoneProperties in mapSpawnZones ) + { + if ( IsValid( zoneProperties.minimapEnt ) ) + zoneProperties.minimapEnt.Destroy() + + zoneProperties.controllingTeam = TEAM_UNASSIGNED + zoneProperties.zoneRating = 0.0 + } } void function AddSpawnZoneTrigger( entity trigger ) { - trigger.s.spawnzoneRating <- 0.0 - spawnStateSpawnzones.mapSpawnzoneTriggers.append( trigger ) + spawnZoneProperties zoneProperties + mapSpawnZones[trigger] <- zoneProperties } -void function SetSpawnZoneRatingFunc( entity functionref( array, int ) ratingFunc ) +bool function TeamHasDirtySpawnzone( int team ) { - spawnStateSpawnzones.spawnzoneRatingFunc = ratingFunc -} - -void function SetShouldCreateMinimapSpawnZones( bool shouldCreateMinimapSpawnzones ) -{ - spawnStateSpawnzones.shouldCreateMinimapSpawnzones = shouldCreateMinimapSpawnzones + foreach ( zone, zoneProperties in mapSpawnZones ) + { + if ( zoneProperties.controllingTeam == team ) + { + int numDeadInZone = 0 + array teamPlayers = GetPlayerArrayOfTeam( team ) + foreach ( entity player in teamPlayers ) + { + if ( Time() - player.p.postDeathThreadStartTime < 20.0 && zone.ContainsPoint( player.p.deathOrigin ) ) + numDeadInZone++ + } + + if ( numDeadInZone < teamPlayers.len() ) + return false + } + } + + return true } -entity function CreateTeamSpawnZoneEntity( entity spawnzone, int team ) +void function CreateTeamSpawnZoneEntity( entity spawnzone, int team ) { entity minimapObj = CreatePropScript( $"models/dev/empty_model.mdl", spawnzone.GetOrigin() ) SetTeam( minimapObj, team ) - minimapObj.Minimap_SetObjectScale( 100.0 / Distance2D( < 0, 0, 0 >, spawnzone.GetBoundingMaxs() ) ) + minimapObj.Minimap_SetObjectScale( Distance2D( < 0, 0, 0 >, spawnzone.GetBoundingMaxs() ) / 16000 ) // 16000 cuz thats the total space Minimap uses minimapObj.Minimap_SetAlignUpright( true ) minimapObj.Minimap_AlwaysShow( TEAM_IMC, null ) minimapObj.Minimap_AlwaysShow( TEAM_MILITIA, null ) @@ -451,67 +464,58 @@ entity function CreateTeamSpawnZoneEntity( entity spawnzone, int team ) minimapObj.Minimap_SetCustomState( eMinimapObject_prop_script.SPAWNZONE_MIL ) minimapObj.DisableHibernation() - return minimapObj + mapSpawnZones[spawnzone].minimapEnt = minimapObj } void function RateSpawnpoints_SpawnZones( int checkClass, array spawnpoints, int team, entity player ) { - if ( spawnStateSpawnzones.spawnzoneRatingFunc == null ) - spawnStateSpawnzones.spawnzoneRatingFunc = DecideSpawnZone_Generic - - // don't use spawnzones if we're using start spawns if ( ShouldStartSpawn( player ) ) { RateSpawnpoints_Generic( checkClass, spawnpoints, team, player ) return } - - entity spawnzone = spawnStateSpawnzones.spawnzoneRatingFunc( spawnStateSpawnzones.mapSpawnzoneTriggers, player.GetTeam() ) - if ( !IsValid( spawnzone ) ) // no spawn zone, use generic algo + + array< entity > zoneTriggers + foreach ( zone, zoneProperties in mapSpawnZones ) + zoneTriggers.append( zone ) + + entity spawnzone = DecideSpawnZone_Generic( zoneTriggers, player.GetTeam() ) + if ( !IsValid( spawnzone ) ) { RateSpawnpoints_Generic( checkClass, spawnpoints, team, player ) return } - // rate spawnpoints foreach ( entity spawn in spawnpoints ) { float rating = 0.0 float distance = Distance2D( spawn.GetOrigin(), spawnzone.GetOrigin() ) if ( distance < Distance2D( < 0, 0, 0 >, spawnzone.GetBoundingMaxs() ) ) - rating = 100.0 - else // max 35 rating if not in zone, rate by closest - rating = 35.0 * ( 1 - ( distance / 5000.0 ) ) + rating = 10.0 + else + rating = 2.0 * ( 1 - ( distance / 3000.0 ) ) - spawn.CalculateRating( checkClass, player.GetTeam(), rating, rating ) + spawn.CalculateRating( checkClass, team, rating, rating * 0.25 ) } } entity function DecideSpawnZone_Generic( array spawnzones, int team ) { - if ( spawnzones.len() == 0 ) + if ( !spawnzones.len() ) return null - // get average team startspawn positions - int spawnCompareTeam = team - if ( HasSwitchedSides() ) - spawnCompareTeam = GetOtherTeam( team ) - - array startSpawns = SpawnPoints_GetPilotStart( spawnCompareTeam ) - array enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( spawnCompareTeam ) ) + array startSpawns = SpawnPoints_GetPilotStart( team ) + array enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( team ) ) - if ( startSpawns.len() == 0 || enemyStartSpawns.len() == 0 ) // ensure we don't crash + if ( !startSpawns.len() || !enemyStartSpawns.len() ) return null - - // get average startspawn position and max dist between spawns - // could probably cache this, tbh, not like it should change outside of halftimes - vector averageFriendlySpawns + + vector averageFriendlySpawns foreach ( entity spawn in startSpawns ) averageFriendlySpawns += spawn.GetOrigin() averageFriendlySpawns /= startSpawns.len() - // get average enemy startspawn position vector averageEnemySpawns foreach ( entity spawn in enemyStartSpawns ) averageEnemySpawns += spawn.GetOrigin() @@ -520,250 +524,87 @@ entity function DecideSpawnZone_Generic( array spawnzones, int team ) float baseDistance = Distance2D( averageFriendlySpawns, averageEnemySpawns ) - bool needNewZone = true - if ( team in spawnStateSpawnzones.activeTeamSpawnzones ) + if ( TeamHasDirtySpawnzone( team ) ) { - foreach ( entity player in GetPlayerArray() ) - { - // couldn't get IsTouching, GetTouchingEntities or enter callbacks to work in testing, so doing this - if ( player.GetTeam() != team && spawnStateSpawnzones.activeTeamSpawnzones[ team ].ContainsPoint( player.GetOrigin() ) ) - break - } - - int numDeadInZone = 0 - array teamPlayers = GetPlayerArrayOfTeam( team ) - foreach ( entity player in teamPlayers ) - { - // check if they died in the zone recently, get a new zone if too many died - if ( Time() - player.p.postDeathThreadStartTime < 15.0 && spawnStateSpawnzones.activeTeamSpawnzones[ team ].ContainsPoint( player.p.deathOrigin ) ) - numDeadInZone++ - } - - // cast to float so result is float - if ( float( numDeadInZone ) / teamPlayers.len() <= 0.1 ) - needNewZone = false - } - - if ( needNewZone ) - { - // find new zone array possibleZones - foreach ( entity spawnzone in spawnStateSpawnzones.mapSpawnzoneTriggers ) + foreach ( zone, zoneProperties in mapSpawnZones ) { - // don't remember if you can do a "value in table.values" sorta thing in squirrel so doing manual lookup - bool spawnzoneTaken = false - foreach ( int otherTeam, entity otherSpawnzone in spawnStateSpawnzones.activeTeamSpawnzones ) - { - if ( otherSpawnzone == spawnzone ) - { - spawnzoneTaken = true - break - } - } - - if ( spawnzoneTaken ) + if ( zoneProperties.controllingTeam == GetOtherTeam( team ) ) continue - // check zone validity - bool spawnzoneEvil = false - foreach ( entity player in GetPlayerArray() ) + bool spawnzoneHasEnemies = false + foreach ( entity enemy in GetPlayerArrayOfEnemies_Alive( team ) ) { - // couldn't get IsTouching, GetTouchingEntities or enter callbacks to work in testing, so doing this - if ( player.GetTeam() != team && spawnzone.ContainsPoint( player.GetOrigin() ) ) + if ( zone.ContainsPoint( enemy.GetOrigin() ) ) { - spawnzoneEvil = true + spawnzoneHasEnemies = true break } } - // don't choose spawnzones that are closer to enemy base than friendly base - // note: vanilla spawns might not necessarily require this, worth checking - if ( !spawnzoneEvil && Distance2D( spawnzone.GetOrigin(), averageFriendlySpawns ) > Distance2D( spawnzone.GetOrigin(), averageEnemySpawns ) ) - spawnzoneEvil = true + if ( !spawnzoneHasEnemies && Distance2D( zone.GetOrigin(), averageFriendlySpawns ) > Distance2D( zone.GetOrigin(), averageEnemySpawns ) ) + spawnzoneHasEnemies = true - if ( spawnzoneEvil ) + if ( spawnzoneHasEnemies ) continue - // rate spawnzone based on distance to frontline Frontline frontline = GetFrontline( team ) - - // prefer spawns close to base pos - float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, spawnzone.GetOrigin() ) / baseDistance ) + float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, zone.GetOrigin() ) / baseDistance ) if ( frontline.friendlyCenter != < 0, 0, 0 > ) { - // rate based on distance to frontline, and then prefer spawns in the same dir from the frontline as the combatdir - rating += rating * ( 1.0 - ( Distance2D( spawnzone.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) - rating *= fabs( frontline.combatDir.y - Normalize( spawnzone.GetOrigin() - averageFriendlySpawns ).y ) + rating += rating * ( 1.0 - ( Distance2D( zone.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) + rating *= fabs( frontline.combatDir.y - Normalize( zone.GetOrigin() - averageFriendlySpawns ).y ) } - spawnzone.s.spawnzoneRating = rating - possibleZones.append( spawnzone ) + zoneProperties.zoneRating = rating + possibleZones.append( zone ) } - if ( possibleZones.len() == 0 ) + if ( !possibleZones.len() ) return null - possibleZones.sort( int function( entity a, entity b ) - { - if ( a.s.spawnzoneRating > b.s.spawnzoneRating ) - return -1 - - if ( b.s.spawnzoneRating > a.s.spawnzoneRating ) - return 1 - - return 0 - } ) - entity chosenZone = possibleZones[ minint( RandomInt( 3 ), possibleZones.len() - 1 ) ] - - if ( spawnStateSpawnzones.shouldCreateMinimapSpawnzones ) - { - entity oldEnt - if ( team in spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts ) - oldEnt = spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] - - spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] <- CreateTeamSpawnZoneEntity( chosenZone, team ) - if ( IsValid( oldEnt ) ) - oldEnt.Destroy() - } + possibleZones.sort( SortPossibleZones ) - spawnStateSpawnzones.activeTeamSpawnzones[ team ] <- chosenZone - } - - return spawnStateSpawnzones.activeTeamSpawnzones[ team ] -} - -// ideally this should be in the gamemode_ctf file, but would need refactors to expose more stuff that's not available there rn -entity function DecideSpawnZone_CTF( array spawnzones, int team ) -{ - if ( spawnzones.len() == 0 ) - return null - - int otherTeam = GetOtherTeam( team ) - array enemyPlayers = GetPlayerArrayOfTeam( otherTeam ) - - // get average team startspawn positions - int spawnCompareTeam = team - if ( HasSwitchedSides() ) - spawnCompareTeam = GetOtherTeam( team ) - - array startSpawns = SpawnPoints_GetPilotStart( spawnCompareTeam ) - array enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( spawnCompareTeam ) ) - - if ( startSpawns.len() == 0 || enemyStartSpawns.len() == 0 ) // ensure we don't crash - return null - - // get average startspawn position and max dist between spawns - // could probably cache this, tbh, not like it should change outside of halftimes - vector averageFriendlySpawns - foreach ( entity spawn in startSpawns ) - averageFriendlySpawns += spawn.GetOrigin() - - averageFriendlySpawns /= startSpawns.len() - - // get average enemy startspawn position - vector averageEnemySpawns - foreach ( entity spawn in enemyStartSpawns ) - averageEnemySpawns += spawn.GetOrigin() - - averageEnemySpawns /= enemyStartSpawns.len() - - float baseDistance = Distance2D( averageFriendlySpawns, averageEnemySpawns ) - - // find new zone - array possibleZones - foreach ( entity spawnzone in spawnStateSpawnzones.mapSpawnzoneTriggers ) - { - // can't choose zone if another team has it - if ( otherTeam in spawnStateSpawnzones.activeTeamSpawnzones && spawnStateSpawnzones.activeTeamSpawnzones[ otherTeam ] == spawnzone ) - continue + entity chosenZone = possibleZones[ minint( RandomInt( 3 ), possibleZones.len() - 1 ) ] - // check zone validity - bool spawnzoneEvil = false - foreach ( entity player in enemyPlayers ) + if ( file.shouldCreateMinimapSpawnzones ) { - // couldn't get IsTouching, GetTouchingEntities or enter callbacks to work in testing, so doing this - if ( spawnzone.ContainsPoint( player.GetOrigin() ) ) + foreach ( zone, zoneProperties in mapSpawnZones ) { - spawnzoneEvil = true - break + if ( chosenZone == zone ) + continue + + if ( IsValid( zoneProperties.minimapEnt ) && zoneProperties.controllingTeam == team ) + zoneProperties.minimapEnt.Destroy() } + + CreateTeamSpawnZoneEntity( chosenZone, team ) } - // don't choose spawnzones that are closer to enemy base than friendly base - if ( !spawnzoneEvil && Distance2D( spawnzone.GetOrigin(), averageFriendlySpawns ) > Distance2D( spawnzone.GetOrigin(), averageEnemySpawns ) ) - spawnzoneEvil = true - - if ( spawnzoneEvil ) - continue - - // rate spawnzone based on distance to frontline - Frontline frontline = GetFrontline( team ) - - // prefer spawns close to base pos - float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, spawnzone.GetOrigin() ) / baseDistance ) - - if ( frontline.friendlyCenter != < 0, 0, 0 > ) + foreach ( zone, zoneProperties in mapSpawnZones ) { - // rate based on distance to frontline, and then prefer spawns in the same dir from the frontline as the combatdir - rating += rating * ( 1.0 - ( Distance2D( spawnzone.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) - rating *= fabs( frontline.combatDir.y - Normalize( spawnzone.GetOrigin() - averageFriendlySpawns ).y ) - - // reduce rating based on players that can currently see the zone - bool hasAppliedInitialLoss = false - foreach ( entity player in enemyPlayers ) - { - // don't trace here, just do an angle check - if ( PlayerCanSee( player, spawnzone, false, 65 ) && Distance2D( player.GetOrigin(), spawnzone.GetOrigin() ) <= 2000.0 ) - { - float distFrac = TraceLineSimple( player.GetOrigin(), spawnzone.GetOrigin(), player ) - - if ( distFrac >= 0.65 ) - { - // give a fairly large loss if literally anyone can see it - if ( !hasAppliedInitialLoss ) - { - rating *= 0.8 - hasAppliedInitialLoss = true - } - - rating *= ( 1.0 / enemyPlayers.len() ) * distFrac - } - } - } + if ( chosenZone == zone ) + continue + + if ( zoneProperties.controllingTeam == team ) + zoneProperties.controllingTeam = TEAM_UNASSIGNED } - spawnzone.s.spawnzoneRating = rating - possibleZones.append( spawnzone ) + mapSpawnZones[chosenZone].controllingTeam = team + return chosenZone } - if ( possibleZones.len() == 0 ) - return null - - possibleZones.sort( int function( entity a, entity b ) - { - if ( a.s.spawnzoneRating > b.s.spawnzoneRating ) - return -1 + return null +} + +int function SortPossibleZones( entity a, entity b ) +{ + if ( mapSpawnZones[a].zoneRating > mapSpawnZones[b].zoneRating ) + return -1 - if ( b.s.spawnzoneRating > a.s.spawnzoneRating ) - return 1 + if ( mapSpawnZones[b].zoneRating > mapSpawnZones[a].zoneRating ) + return 1 - return 0 - } ) - entity chosenZone = possibleZones[ minint( RandomInt( 3 ), possibleZones.len() - 1 ) ] - - if ( spawnStateSpawnzones.shouldCreateMinimapSpawnzones ) - { - entity oldEnt - if ( team in spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts ) - oldEnt = spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] - - spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] <- CreateTeamSpawnZoneEntity( chosenZone, team ) - if ( IsValid( oldEnt ) ) - oldEnt.Destroy() - } - - spawnStateSpawnzones.activeTeamSpawnzones[ team ] <- chosenZone - - return spawnStateSpawnzones.activeTeamSpawnzones[ team ] + return 0 } \ No newline at end of file diff --git a/README.md b/README.md index 7b6dfaf02..4dbbd6edd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Issues in this repository should be created if they are related to these domains - `Northstar.Client` - Localisation files, UI and client-side scripts. - `Northstar.Coop` - Soon™. - `Northstar.Custom` - Northstar custom content. -- `Northstar.CustomServer` - Server config files and scripts necessary for multiplayer. +- `Northstar.CustomServers` - Server config files and scripts necessary for multiplayer. ### Translating