aboutsummaryrefslogtreecommitdiff
path: root/Northstar.Custom/mod/scripts
diff options
context:
space:
mode:
authorGeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com>2024-08-14 17:31:06 +0200
committerGitHub <noreply@github.com>2024-08-14 17:31:06 +0200
commitfa4e319c0b60cd68a3dccaa4322e3c35cfa1e385 (patch)
tree4a78391c5008a4aa50d81fbca5d9f26bb475cd96 /Northstar.Custom/mod/scripts
parentbcec5a5e9edd2a2af3a017ea4b250a9ba1112e6f (diff)
parent7aa3958ccd8e32970736654dfae0c7a87f0798bb (diff)
downloadNorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.tar.gz
NorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.zip
Merge branch 'main' into permanent-amped-weapons-fix-pr
Diffstat (limited to 'Northstar.Custom/mod/scripts')
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/_droppod_spawn.gnut18
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/_event_models.gnut21
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/_testing.nut302
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut4
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/earn_meter/cl_earn_meter.gnut6
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball.gnut3
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut2391
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_gg.gnut8
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut79
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut15
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_tffa.gnut5
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/cl_gamemode_fw_custom.nut13
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut71
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut1226
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut3
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/sh_damage_types.nut778
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut505
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/sh_northstar_custom_precache.gnut11
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/sh_northstar_http_requests.gnut235
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/sh_northstar_safe_io.gnut83
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/titan/sh_titan.gnut2
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/ui/ns_custom_mod_settings.gnut8
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut1033
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/weapons/mp_titanweapon_arc_cannon.nut226
-rw-r--r--Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt349
-rw-r--r--Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt2
-rw-r--r--Northstar.Custom/mod/scripts/weapons/mp_weapon_shotgun_doublebarrel.txt405
27 files changed, 7746 insertions, 56 deletions
diff --git a/Northstar.Custom/mod/scripts/vscripts/_droppod_spawn.gnut b/Northstar.Custom/mod/scripts/vscripts/_droppod_spawn.gnut
index 7447fc59..5bc75db2 100644
--- a/Northstar.Custom/mod/scripts/vscripts/_droppod_spawn.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/_droppod_spawn.gnut
@@ -14,7 +14,10 @@ void function DropPodSpawn_Init()
void function CleanupSpawningDropPods()
{
foreach ( entity pod in file.droppods )
- pod.Destroy()
+ {
+ if( IsValid( pod ) )
+ pod.Destroy()
+ }
file.droppods.clear()
}
@@ -22,6 +25,7 @@ void function CleanupSpawningDropPods()
void function SpawnPlayersInDropPod( array< entity > players, vector targetOrigin, vector angles, float destructionTime = -1 )
{
entity pod = CreateDropPod( targetOrigin, angles )
+ pod.EndSignal( "OnDestroy" )
file.droppods.append( pod )
svGlobal.levelEnt.EndSignal( "CleanUpEntitiesForRoundEnd" )
@@ -35,9 +39,11 @@ void function SpawnPlayersInDropPod( array< entity > players, vector targetOrigi
foreach ( entity player in players )
{
+ if( !IsValid( player ) )
+ continue
if ( !IsAlive( player ) )
player.RespawnPlayer( null )
-
+
player.SetOrigin( pod.GetOrigin() )
player.SetAngles( pod.GetAngles() )
player.SetParent( pod )
@@ -49,8 +55,12 @@ void function SpawnPlayersInDropPod( array< entity > players, vector targetOrigi
// wait for this
LaunchAnimDropPod( pod, "pod_testpath", targetOrigin, angles )
+ if( !GamePlaying() )
+ return
foreach ( entity player in players )
{
+ if( !IsValid( player ) )
+ continue
player.ClearParent()
player.ClearViewEntity()
player.UnfreezeControlsOnServer()
@@ -61,8 +71,12 @@ void function SpawnPlayersInDropPod( array< entity > players, vector targetOrigi
WaitFrame()
vector doorPos = pod.GetAttachmentOrigin( pod.LookupAttachment( "hatch" ) )
+ if( !GamePlaying() )
+ return
foreach ( entity player in players )
{
+ if( !IsValid( player ) )
+ continue
vector viewAngles = doorPos - player.GetOrigin()
viewAngles.x = 3.0
diff --git a/Northstar.Custom/mod/scripts/vscripts/_event_models.gnut b/Northstar.Custom/mod/scripts/vscripts/_event_models.gnut
new file mode 100644
index 00000000..0802d769
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/_event_models.gnut
@@ -0,0 +1,21 @@
+global function EventModelsInit
+
+void function EventModelsInit()
+{
+ if( !GetConVarBool( "ns_show_event_models" ) )
+ return
+
+ table timeParts = GetUnixTimeParts()
+ int month = expect int( timeParts[ "month" ] )
+ int day = expect int( timeParts[ "day" ] )
+
+ // 18th December to 6th January
+ if( ( ( month == 12 ) && ( day >= 18 ) ) || ( ( month == 1 ) && ( day <= 6 ) ) )
+ {
+ PrecacheModel( $"models/northstartee/winter_holiday_tree.mdl" )
+ PrecacheModel( $"models/northstartree/winter_holiday_floor.mdl" )
+
+ CreatePropDynamic( $"models/northstartree/winter_holiday_tree.mdl", < -60, 740, 30 >, < 0, 0, 0 >, SOLID_VPHYSICS, 1000 )
+ CreatePropDynamic( $"models/northstartree/winter_holiday_floor.mdl", < -60, 740, 30 >, < 0, 0, 0 >, SOLID_VPHYSICS, 1000 )
+ }
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/_testing.nut b/Northstar.Custom/mod/scripts/vscripts/_testing.nut
new file mode 100644
index 00000000..15bcf18b
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/_testing.nut
@@ -0,0 +1,302 @@
+global function Testing_Init
+global function RunAllTests
+global function RunAllTests_SaveToFile
+global function RunTestsByCategory
+global function RunTestByCategoryAndName
+
+global function AddTest
+
+struct TestInfo
+{
+ string testName
+ var functionref() callback
+ // whether the test completed successfully
+ // if this is true, actualResult is valid
+ bool completed
+ // var not string because then i can just set it to an exception
+ // which print can then handle
+ var error
+ // whether the test is considered successful
+ var expectedResult
+ var actualResult
+ bool passed
+}
+
+struct {
+ table< string, array< TestInfo > > tests = {}
+} file
+
+void function Testing_Init()
+{
+ // tests for the testing functions :)
+ //AddTest( "Example Tests", "example succeeding test", ExampleTest_ReturnsTrue, true )
+ //AddTest( "Example Tests", "example failing test", ExampleTest_ReturnsFalse, true )
+ //AddTest( "Example Tests", "example erroring test", ExampleTest_ThrowsError, true )
+ //AddTest( "Example Tests", "example test with args", var function() {
+ // return ExampleTest_HasArgs_ReturnsNonVar( 2, 3 )
+ //}, 6 )
+}
+
+int function ExampleTest_HasArgs_ReturnsNonVar( int first, int second )
+{
+ return first * second
+}
+
+var function ExampleTest_ReturnsFalse()
+{
+ return false
+}
+
+var function ExampleTest_ReturnsTrue()
+{
+ return true
+}
+
+var function ExampleTest_ThrowsError()
+{
+ throw "Example exception"
+ return null
+}
+
+void function RunAllTests_SaveToFile()
+{
+ RunAllTests()
+
+ #if UI
+ string fileName = "ns-unit-tests-UI.json"
+ #elseif CLIENT
+ string fileName = "ns-unit-tests-CLIENT.json"
+ #elseif SERVER
+ string fileName = "ns-unit-tests-SERVER.json"
+ #endif
+
+ // cant encode structs so have to reconstruct a table manually from the structs
+ table out = {}
+ foreach ( category, tests in file.tests )
+ {
+ array categoryResults = []
+ foreach ( test in tests )
+ {
+ table testTable = {}
+ testTable[ "name" ] <- test.testName
+ testTable[ "completed" ] <- test.completed
+ testTable[ "passed" ] <- test.passed
+ if ( !test.completed )
+ testTable[ "error" ] <- test.error
+ else if ( !test.passed )
+ {
+ testTable[ "expectedResult" ] <- test.expectedResult
+ testTable[ "actualResult" ] <- test.actualResult
+ }
+
+ categoryResults.append( testTable )
+ }
+ out[ category ] <- categoryResults
+ }
+
+ NSSaveJSONFile( fileName, out )
+}
+
+void function RunAllTests()
+{
+ printt( "Running all tests!" )
+
+ foreach ( category, categoryTests in file.tests )
+ {
+ foreach ( test in categoryTests )
+ {
+ RunTest( test )
+ }
+ }
+
+ PrintAllTestResults()
+}
+
+void function RunTestsByCategory( string category )
+{
+ if ( !( category in file.tests ) )
+ {
+ printt( format( "Category '%s' has no tests registered", category ) )
+ return
+ }
+
+ foreach ( categoryTest in file.tests[ category ] )
+ {
+ RunTest( categoryTest )
+ }
+}
+
+void function RunTestByCategoryAndName( string category, string testName )
+{
+ // find test
+ if ( !( category in file.tests ) )
+ {
+ printt( format( "Category '%s' has no tests registered", category ) )
+ return
+ }
+
+ TestInfo ornull foundTest = null
+ foreach ( categoryTest in file.tests[ category ] )
+ {
+ if ( categoryTest.testName == testName )
+ {
+ foundTest = categoryTest
+ break
+ }
+ }
+
+ if ( !foundTest )
+ {
+ printt( format( "Category '%s' does not contain a test with name '%s'", category, testName ) )
+ return
+ }
+
+ expect TestInfo( foundTest )
+
+ printt( "Running test!" )
+ // run test
+ RunTest( foundTest )
+ // print result
+ PrintTestResult( foundTest )
+}
+
+void function RunTest( TestInfo test )
+{
+ test.completed = false
+ test.passed = false
+ test.actualResult = null
+ test.error = ""
+
+ try
+ {
+ test.actualResult = test.callback()
+ test.completed = true
+ test.passed = test.actualResult == test.expectedResult
+ }
+ catch ( exception )
+ {
+ test.completed = false
+ test.error = exception
+ }
+}
+
+void function PrintAllTestResults()
+{
+ int totalSucceeded = 0
+ int totalFailed = 0
+ int totalErrored = 0
+
+ foreach ( category, categoryTests in file.tests )
+ {
+ int categorySucceeded = 0
+ int categoryFailed = 0
+ int categoryErrored = 0
+
+ printt( format( "Results for category: '%s'", category ) )
+ foreach ( test in categoryTests )
+ {
+ if ( test.completed )
+ {
+ if ( test.passed )
+ {
+ printt( "\t", test.testName, "- Passed!" )
+ categorySucceeded++
+ }
+ else
+ {
+ printt( "\t", test.testName, "- Failed!" )
+ printt( "\t\tExpected:", test.expectedResult )
+ printt( "\t\tActual: ", test.actualResult )
+ categoryFailed++
+ }
+ }
+ else
+ {
+ printt( "\t", test.testName, "- Errored!" )
+ printt( "\t\tError:", test.error )
+ categoryErrored++
+ }
+ }
+
+ printt( "Succeeded:", categorySucceeded, "Failed:", categoryFailed, "Errored:", categoryErrored )
+
+ totalSucceeded += categorySucceeded
+ totalFailed += categoryFailed
+ totalErrored += categoryErrored
+ }
+
+ printt( "TOTAL SUCCEEDED:", totalSucceeded, "TOTAL FAILED:", totalFailed, "TOTAL ERRORED:", totalErrored )
+}
+
+void function PrintCategoryResults( string category )
+{
+ int categorySucceeded = 0
+ int categoryFailed = 0
+ int categoryErrored = 0
+
+ printt( format( "Results for category: '%s'", category ) )
+ foreach ( test in file.tests[ category ] )
+ {
+ if ( test.completed )
+ {
+ if ( test.passed )
+ {
+ printt( "\t", test.testName, "- Passed!" )
+ categorySucceeded++
+ }
+ else
+ {
+ printt( "\t", test.testName, "- Failed!" )
+ printt( "\t\tExpected:", test.expectedResult )
+ printt( "\t\tActual: ", test.actualResult )
+ categoryFailed++
+ }
+ }
+ else
+ {
+ printt( "\t", test.testName, "- Errored!" )
+ printt( "\t\tError:", test.error )
+ categoryErrored++
+ }
+ }
+
+ printt( "Succeeded:", categorySucceeded, "Failed:", categoryFailed, "Errored:", categoryErrored )
+}
+
+void function PrintTestResult( TestInfo test )
+{
+ string resultString = test.testName
+
+ if ( test.completed )
+ {
+ if ( test.passed )
+ resultString += " - Passed!"
+ else
+ {
+ resultString += " - Failed!"
+ resultString += "\n\tExpected: " + test.expectedResult
+ resultString += "\n\tActual: " + test.actualResult
+ }
+ }
+ else
+ {
+ resultString += " - Not completed!"
+ resultString += "\n\tError: " + test.error
+ }
+
+ printt( resultString )
+}
+
+void function AddTest( string testCategory, string testName, var functionref() testFunc, var expectedResult )
+{
+ TestInfo newTest
+ newTest.testName = testName
+ newTest.callback = testFunc
+ newTest.expectedResult = expectedResult
+
+ // create the test category if it doesn't exist
+ if ( !( testCategory in file.tests ) )
+ file.tests[ testCategory ] <- [ newTest ]
+ else
+ file.tests[ testCategory ].append( newTest )
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut b/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut
index ac9ffab3..37d4356f 100644
--- a/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/burnmeter/sh_burnmeter.gnut
@@ -188,7 +188,7 @@ BurnReward function BurnReward_GetRandom()
string ref = burn.allowedCards.getrandom().ref
#if SERVER || CLIENT
- if ( !EarnMeterMP_IsTitanEarnGametype() )
+ if ( Riff_TitanAvailability() == eTitanAvailability.Never )
ref = BurnMeter_GetNoTitansReplacement( ref )
if ( GetCurrentPlaylistVarInt( "featured_mode_all_ticks", 0 ) >= 1 )
@@ -211,7 +211,7 @@ string function GetSelectedBurnCardRef( entity player )
#endif
#if SERVER || CLIENT
- if ( !EarnMeterMP_IsTitanEarnGametype() )
+ if ( Riff_TitanAvailability() == eTitanAvailability.Never )
ref = BurnMeter_GetNoTitansReplacement( ref )
if ( GetCurrentPlaylistVarInt( "featured_mode_all_ticks", 0 ) >= 1 )
diff --git a/Northstar.Custom/mod/scripts/vscripts/earn_meter/cl_earn_meter.gnut b/Northstar.Custom/mod/scripts/vscripts/earn_meter/cl_earn_meter.gnut
index 16908362..3971d2be 100644
--- a/Northstar.Custom/mod/scripts/vscripts/earn_meter/cl_earn_meter.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/earn_meter/cl_earn_meter.gnut
@@ -335,7 +335,11 @@ void function EarnMeter_Update()
break
entity soul = player.GetTitanSoul()
- entity core = player.GetOffhandWeapons()[3]
+ entity core = player.GetOffhandWeapon( OFFHAND_EQUIPMENT )
+
+ if ( !IsValid( core ) )
+ break
+
string coreName = core.GetWeaponClassName()
array<string> coreMods = core.GetMods()
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball.gnut
index 019bcc7d..409d5ec0 100644
--- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fastball.gnut
@@ -1,5 +1,6 @@
untyped
global function GamemodeFastball_Init
+global function FastballAddPanelSpawnsForLevel
struct {
// first panel is a, second is b, third is c
@@ -282,7 +283,7 @@ function FastballOnPanelHacked( panel, player )
// respawn dead players
foreach ( entity deadPlayer in GetPlayerArrayOfTeam( player.GetTeam() ) )
{
- if ( !IsAlive( deadPlayer ) && !IsPrivateMatchSpectator( player ) )
+ if ( !IsAlive( deadPlayer ) && !IsPrivateMatchSpectator( deadPlayer ) )
{
deadPlayer.SetOrigin( panel.s.startOrigin )
deadPlayer.RespawnPlayer( null )
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut
new file mode 100644
index 00000000..fa66c2f7
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut
@@ -0,0 +1,2391 @@
+untyped
+global function GamemodeFW_Init
+
+// spawn points
+global function RateSpawnpointsPilot_FW
+global function RateSpawnpointsTitan_FW
+//global function RateSpawnpoints_FW
+
+// for battery_port.gnut to work
+global function FW_ReplaceMegaTurret
+
+// fw specific titanfalls
+global function FW_IsPlayerInFriendlyTerritory
+global function FW_IsPlayerInEnemyTerritory
+
+// Callbacks for mods to reduce harvester damage of modded weapons
+global function FW_AddHarvesterDamageSourceModifier
+global function FW_RemoveHarvesterDamageSourceModifier
+
+// you need to deal this much damage to trigger "FortWarTowerDamage" score event
+const int FW_HARVESTER_DAMAGE_SEGMENT = 5250
+
+// basically needs to match "waves count - bosswaves count"
+const int FW_MAX_LEVELS = 3
+
+// to confirm it's a npc from camps..
+const string FW_NPC_SCRIPTNAME = "fw_npcsFromCamp"
+const int FW_AI_TEAM = TEAM_BOTH
+const float WAVE_STATE_TRANSITION_TIME = 5.0
+
+// from sh_gamemode_fw, if half of these npcs cleared in one camp, it gets escalate
+const int FW_GRUNT_COUNT = 36//32
+const int FW_SPECTRE_COUNT = 24
+const int FW_REAPER_COUNT = 2
+
+// max deployment each camp
+const int FW_GRUNT_MAX_DEPLOYED = 8
+const int FW_SPECTRE_MAX_DEPLOYED = 8
+const int FW_REAPER_MAX_DEPLOYED = 1
+
+// if other camps been cleaned many times, we levelDown
+const int FW_CAMP_IGNORE_NEEDED = 2
+
+// debounce for showing damaged infos
+const float FW_HARVESTER_DAMAGED_DEBOUNCE = 5.0
+const float FW_TURRET_DAMAGED_DEBOUNCE = 2.0
+
+global HarvesterStruct& fw_harvesterMlt
+global HarvesterStruct& fw_harvesterImc
+
+// these are not using respawn's remaining code( sh_gamemode_fw.nut )!
+
+// respawn already have a FW_TowerData struct! this struct is only for score events
+struct HarvesterDamageStruct
+{
+ float recentDamageTime
+ int storedDamage
+}
+
+struct TurretSiteStruct
+{
+ entity site
+ entity turret
+ entity minimapstate
+ string turretflagid
+}
+
+// respawn already have a FW_CampData, FW_WaveOrigin and FW_SpawnData struct!
+struct CampSiteStruct
+{
+ entity camp
+ entity info
+ entity tracker
+ array<entity> validDropPodSpawns
+ array<entity> validTitanSpawns
+ string campId // "A", "B", "C"
+ int npcsAlive
+ int ignoredSinceLastClean
+}
+
+struct CampSpawnStruct
+{
+ string spawnContent // what npcs to spawn
+ int maxSpawnCount // max spawn count on this camp
+ int countPerSpawn // how many npcs to deploy per spawn, for droppods most be 4
+ int killsToEscalate // how many kills needed to escalate
+}
+
+struct
+{
+ array<HarvesterStruct> harvesters
+
+ // Stores damage source IDs and the modifier applied to them when they damage a harvester
+ table< int, float > harvesterDamageSourceMods
+
+ // save camp's info_target, we spawn camps after game starts, or player's first life won't show up correct camp icons
+ array<entity> camps
+
+ array<entity> fwTerritories
+
+ array<TurretSiteStruct> turretsites
+
+ array<CampSiteStruct> fwCampSites
+
+ // respawn already have a FW_TowerData struct! this table is only for score events
+ table< entity, HarvesterDamageStruct > playerDamageHarvester // team, table< player, time >
+
+ // this is for saving territory's connecting time, try not to make faction dialogues play together
+ table< int, float > teamTerrLastConnectTime // team, time
+
+ // unused
+ array<entity> etitaninmlt
+ array<entity> etitaninimc
+
+ entity harvesterMlt_info
+ entity harvesterImc_info
+
+ table<int, CampSpawnStruct> fwNpcLevel // basically use to powerup certian camp, sync with alertLevel
+ table< string, table< string, int > > trackedCampNPCSpawns
+}file
+
+void function GamemodeFW_Init()
+{
+ // _battery_port.gnut needs this
+ RegisterSignal( "BatteryActivate" )
+
+ AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] )
+ AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] )
+
+ AddCallback_EntitiesDidLoad( LoadEntities )
+ AddCallback_GameStateEnter( eGameState.Prematch, OnFWGamePrematch )
+ AddCallback_GameStateEnter( eGameState.Playing, OnFWGamePlaying )
+
+ AddSpawnCallback( "item_powerup", FWAddPowerUpIcon )
+ AddSpawnCallback( "npc_turret_mega", OnFWTurretSpawned )
+
+ AddCallback_OnClientConnected( OnFWPlayerConnected )
+ AddCallback_PlayerClassChanged( OnFWPlayerClassChanged )
+ AddCallback_OnPlayerKilled( OnFWPlayerKilled )
+ AddCallback_OnPilotBecomesTitan( OnFWPilotBecomesTitan )
+ AddCallback_OnTitanBecomesPilot( OnFWTitanBecomesPilot )
+
+ ScoreEvent_SetupEarnMeterValuesForMixedModes()
+ SetRecalculateRespawnAsTitanStartPointCallback( FW_ForcedTitanStartPoint )
+ SetRecalculateTitanReplacementPointCallback( FW_ReCalculateTitanReplacementPoint )
+ SetRequestTitanAllowedCallback( FW_RequestTitanAllowed )
+}
+
+
+
+//////////////////////////
+///// HACK FUNCTIONS /////
+//////////////////////////
+
+const array<string> HACK_CLEANUP_MAPS =
+[
+ "mp_grave",
+ "mp_homestead",
+ "mp_complex3"
+]
+
+//if npcs outside the map try to fire( like in death animation ), it will cause a engine error
+
+// in mp_grave, npcs will sometimes stuck underground
+const float GRAVE_CHECK_HEIGHT = 1700 // the map's lowest ground is 1950+, npcs will stuck under -4000 or -400
+// in mp_homestead, npcs will sometimes stuck in the sky
+const float HOMESTEAD_CHECK_HIEGHT = 8000 // the map's highest part is 7868+, npcs will stuck above 13800+
+// in mp_complex3, npcs will sometimes stuck in the sky
+const float COMPLEX_CHECK_HEIGHT = 7000 // the map's highest part is 6716+, npcs will stuck above 9700+
+
+// do a hack
+void function HACK_ForceDestroyNPCs()
+{
+ thread HACK_ForceDestroyNPCs_Threaded()
+}
+
+void function HACK_ForceDestroyNPCs_Threaded()
+{
+ string mapName = GetMapName()
+ if( !( HACK_CLEANUP_MAPS.contains( mapName ) ) )
+ return
+
+ while( true )
+ {
+ if( mapName == "mp_grave" )
+ {
+ foreach( entity npc in GetNPCArray() )
+ {
+ if( npc.GetOrigin().z <= GRAVE_CHECK_HEIGHT )
+ {
+ npc.ClearParent()
+ npc.Destroy()
+ }
+ }
+ }
+ if( mapName == "mp_homestead" )
+ {
+ foreach( entity npc in GetNPCArray() )
+ {
+ // neither spawning from droppod nor hotdropping
+ if( !IsValid( npc.GetParent() ) && !npc.e.isHotDropping )
+ {
+ if( npc.GetOrigin().z >= HOMESTEAD_CHECK_HIEGHT )
+ {
+ npc.Destroy()
+ }
+ }
+ }
+ }
+ if( mapName == "mp_complex3" )
+ {
+ foreach( entity npc in GetNPCArray() )
+ {
+ // neither spawning from droppod nor hotdropping
+ if( !IsValid( npc.GetParent() ) && !npc.e.isHotDropping )
+ {
+ if( npc.GetOrigin().z >= COMPLEX_CHECK_HEIGHT )
+ {
+ npc.Destroy()
+ }
+ }
+ }
+ }
+ WaitFrame()
+ }
+}
+
+//////////////////////////////
+///// HACK FUNCTIONS END /////
+//////////////////////////////
+
+
+
+////////////////////////////////
+///// SPAWNPOINT FUNCTIONS /////
+////////////////////////////////
+
+void function RateSpawnpointsPilot_FW( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ array<entity> startSpawns = SpawnPoints_GetPilotStart( team )
+ RateSpawnpoints_FW( startSpawns, checkClass, spawnpoints, team, player )
+}
+
+void function RateSpawnpointsTitan_FW( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ array<entity> startSpawns = SpawnPoints_GetTitanStart( team )
+ RateSpawnpoints_FW( startSpawns, checkClass, spawnpoints, team, player )
+}
+
+void function RateSpawnpoints_FW( array<entity> startSpawns, int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ if ( HasSwitchedSides() )
+ team = GetOtherTeam( team )
+
+ // average out startspawn positions
+ vector averageFriendlySpawns
+ foreach ( entity spawnpoint in startSpawns )
+ averageFriendlySpawns += spawnpoint.GetOrigin()
+
+ averageFriendlySpawns /= startSpawns.len()
+
+ entity friendlyTerritory
+ foreach ( entity territory in file.fwTerritories )
+ {
+ if ( team == territory.GetTeam() )
+ {
+ friendlyTerritory = territory
+ break
+ }
+ }
+
+ vector ratingPos
+ if ( IsValid( friendlyTerritory ) )
+ ratingPos = friendlyTerritory.GetOrigin()
+ else
+ ratingPos = averageFriendlySpawns
+
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ // idk about magic number here really
+ float rating = 1.0 - ( Distance2D( spawnpoint.GetOrigin(), ratingPos ) / 1000.0 )
+ spawnpoint.CalculateRating( checkClass, player.GetTeam(), rating, rating )
+ }
+}
+
+////////////////////////////////////
+///// SPAWNPOINT FUNCTIONS END /////
+////////////////////////////////////
+
+
+
+//////////////////////////////
+///// CALLBACK FUNCTIONS /////
+//////////////////////////////
+
+void function OnFWGamePrematch()
+{
+ InitFWScoreEvents()
+ FW_createHarvester()
+ InitFWCampSites()
+ InitCampSpawnerLevel()
+}
+
+void function OnFWGamePlaying()
+{
+ startFWHarvester()
+ FWAreaThreatLevelThink()
+ StartFWCampThink()
+ InitTurretSettings()
+ FWPlayerObjectiveState()
+
+ HACK_ForceDestroyNPCs()
+}
+
+void function OnFWPlayerConnected( entity player )
+{
+ InitFWPlayers( player )
+}
+
+void function OnFWPlayerClassChanged( entity player )
+{
+ // give player a friendly highlight
+ Highlight_SetFriendlyHighlight( player, "fw_friendly" )
+}
+
+void function OnFWPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ HandleFWPlayerKilledScoreEvent( victim, attacker )
+}
+
+void function OnFWPilotBecomesTitan( entity player, entity titan )
+{
+ // objective stuff
+ SetTitanObjective( player, titan )
+}
+
+void function OnFWTitanBecomesPilot( entity player, entity titan )
+{
+ // objective stuff
+ SetPilotObjective( player, titan )
+}
+
+//////////////////////////////////
+///// CALLBACK FUNCTIONS END /////
+//////////////////////////////////
+
+
+/////////////////////////////////
+///// SCORE EVENT FUNCTIONS /////
+/////////////////////////////////
+
+void function InitFWScoreEvents()
+{
+ // common scoreEvents
+ ScoreEvent_SetEarnMeterValues( "KillHeavyTurret", 0.0, 0.20 ) // can only adds to titan's in this mode
+
+ // fw special: save for later use of scoreEvents
+
+ // combat
+ ScoreEvent_SetEarnMeterValues( "FortWarAssault", 0.0, 0.05, 0.0 ) // titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarDefense", 0.0, 0.05, 0.0 ) // titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarPerimeterDefense", 0.0, 0.05 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarSiege", 0.0, 0.05 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarSnipe", 0.0, 0.05 ) // unused
+
+ // constructions
+ ScoreEvent_SetEarnMeterValues( "FortWarBaseConstruction", 0.0, 0.15 )
+ ScoreEvent_SetEarnMeterValues( "FortWarForwardConstruction", 0.0, 0.15 )
+ ScoreEvent_SetEarnMeterValues( "FortWarInvasiveConstruction", 0.0, 0.25 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarResourceDenial", 0.0, 0.05 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarSecuringGatheredResources", 0.0, 0.05 ) // unused
+
+ // tower
+ ScoreEvent_SetEarnMeterValues( "FortWarTowerDamage", 0.0, 0.10, 0.0 ) // using the const FW_HARVESTER_DAMAGE_SEGMENT, titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarTowerDefense", 0.0, 0.10, 0.0 ) // titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarShieldDestroyed", 0.0, 0.15 )
+
+ // turrets
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_One", 0.0, 0.15, 0.5 ) // give more meter if no turret left
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Two", 0.0, 0.15, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Three", 0.0, 0.10, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Four", 0.0, 0.10, 0.5 ) // give less meter if controlled most turrets
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Five", 0.0, 0.05, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Six", 0.0, 0.05, 0.5 )
+}
+
+// consider this means victim recently damaged harvester
+const float TOWER_DEFENSE_REQURED_TIME = 10.0
+
+void function HandleFWPlayerKilledScoreEvent( entity victim, entity attacker )
+{
+ // this function only handles player's kills
+ if( !attacker.IsPlayer() )
+ return
+
+ // suicide don't get scores
+ if( attacker == victim )
+ return
+
+ int attackerTeam = attacker.GetTeam()
+ int victimTeam = victim.GetTeam()
+
+ string scoreEvent = ""
+ int secondaryScore = 0
+ entity attackerHarvester = FW_GetTeamHarvesterProp( attackerTeam )
+
+ if( FW_IsPlayerInEnemyTerritory( victim ) ) // victim is in enemy territory
+ {
+ scoreEvent = "FortWarDefense" // enemy earn score from defense
+ secondaryScore = POINTVALUE_FW_DEFENSE
+ }
+
+ if( FW_IsPlayerInFriendlyTerritory( victim ) ) // victim is in friendly territory
+ {
+ scoreEvent = "FortWarAssault" // enemy earn score from assault
+ secondaryScore = POINTVALUE_FW_ASSAULT
+ }
+
+ if( victim in file.playerDamageHarvester ) // victim has damaged the harvester this life
+ {
+ float damageTime = file.playerDamageHarvester[ victim ].recentDamageTime
+
+ // is victim recently damaged havester?
+ if( damageTime + TOWER_DEFENSE_REQURED_TIME >= Time() )
+ {
+ scoreEvent = "FortWarTowerDefense" // you defend the tower!
+ secondaryScore = POINTVALUE_FW_TOWER_DEFENSE
+ }
+
+ }
+
+ if( scoreEvent != "" )
+ {
+ AddPlayerScore( attacker, scoreEvent, victim )
+ attacker.AddToPlayerGameStat( PGS_DEFENSE_SCORE, secondaryScore )
+ }
+}
+
+/////////////////////////////////////
+///// SCORE EVENT FUNCTIONS END /////
+/////////////////////////////////////
+
+
+
+//////////////////////////////////////
+///// FACTION DIALOGUE FUNCTIONS /////
+//////////////////////////////////////
+
+const float FW_TERRYTORY_DIALOGUE_DEBOUNCE = 5.0
+
+// WORKING IN PROGRESS
+bool function TryFWTerritoryDialogue( entity territory, entity player )
+{
+ bool thisTimeIsTitan = player.IsTitan()
+ int terrTeam = territory.GetTeam()
+ int enemyTeam = GetOtherTeam( terrTeam )
+ bool sameTeam = terrTeam == player.GetTeam()
+ bool isInDebounce = file.teamTerrLastConnectTime[ terrTeam ] + FW_TERRYTORY_DIALOGUE_DEBOUNCE >= Time()
+
+ // the territory trigger will only save players and titans
+ array<entity> allEntsInside = GetAllEntitiesInTrigger( territory )
+ allEntsInside.removebyvalue( null ) // since we're using a fake trigger, need to check this
+ array<entity> friendliesInside // this means territory's friendly team
+ array<entity> enemiesInside // this means territory's enemy team
+ array<entity> enemyTitansInside
+ foreach( entity ent in allEntsInside )
+ {
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.GetTeam() == terrTeam )
+ friendliesInside.append( ent )
+ }
+ foreach( entity ent in allEntsInside )
+ {
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.GetTeam() != terrTeam )
+ enemiesInside.append( ent )
+ }
+ foreach( entity enemy in enemiesInside )
+ {
+ if( !IsValid( enemy ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( enemy.IsTitan() )
+ enemyTitansInside.append( enemy )
+ }
+
+ print( "enemy in territory: " + string( enemiesInside.len() ) )
+ print( "friendly in territory: " + string( friendliesInside.len() ) )
+
+ print( "sameTeam: " + string( sameTeam ) )
+ print( "isInDebounce: " + string( isInDebounce ) )
+ print( "thisTimeIsTitan: " + string( thisTimeIsTitan ) )
+
+ if( enemiesInside.len() > 3 || friendliesInside.len() > 1 ) // already have some players triggered dialogue
+ return false
+
+ // notify player enemy's behaves
+ if( !sameTeam ) // player is not the same team as territory
+ {
+ // consider this means all enemies has left friendly territory, should use a debounce
+ if( enemiesInside.len() == 0 && !isInDebounce )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terEnemyExpelled", terrTeam )
+ return true
+ }
+ // has more than 3 titans inside including new one, ignores debounce
+ else if( enemyTitansInside.len() >= 3 && thisTimeIsTitan )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terPresentEnemyTitans", terrTeam )
+ return true
+ }
+ // only the player inside terrytory
+ else if( enemyTitansInside.len() == 1 )
+ {
+ // entered territory as titan, ignores debounce
+ if( thisTimeIsTitan )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terEnteredEnemyPilot", terrTeam )
+ return true
+ }
+ // entered territory as pilot
+ else if( !isInDebounce )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terEnteredEnemyPilot", terrTeam )
+ return true
+ }
+ }
+
+ // notify player friendly's behaves
+ // consider this means all friendlies has left enemy territory
+ if( friendliesInside.len() == 0 && !sameTeam && !isInDebounce )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terFriendlyExpelled", terrTeam )
+ return true
+ }
+ }
+
+ return false
+}
+
+//////////////////////////////////////////
+///// FACTION DIALOGUE FUNCTIONS END /////
+//////////////////////////////////////////
+
+
+
+/////////////////////////////////////////
+///// GAMEMODE INITIALIZE FUNCTIONS /////
+/////////////////////////////////////////
+
+void function LoadEntities()
+{
+ // info_target
+ foreach ( entity info_target in GetEntArrayByClass_Expensive( "info_target" ) )
+ {
+ if( info_target.HasKey( "editorclass" ) )
+ {
+ switch( info_target.kv.editorclass )
+ {
+ case "info_fw_team_tower":
+ if ( info_target.GetTeam() == TEAM_IMC )
+ {
+ file.harvesterImc_info = info_target
+ //print("fw_tower tracker spawned")
+ }
+ if ( info_target.GetTeam() == TEAM_MILITIA )
+ {
+ file.harvesterMlt_info = info_target
+ //print("fw_tower tracker spawned")
+ }
+ break
+ case "info_fw_camp":
+ file.camps.append( info_target )
+ //InitCampTracker( info_target )
+ //print("fw_camp spawned")
+ break
+ case "info_fw_turret_site":
+ string idString = expect string(info_target.kv.turretId)
+ int id = int( info_target.kv.turretId )
+ //print("info_fw_turret_siteID : " + idString )
+
+ // set this for replace function to find
+ TurretSiteStruct turretsite
+ file.turretsites.append( turretsite )
+
+ turretsite.site = info_target
+
+ // create turret, spawn with no team and set it after game starts
+ entity turret = CreateNPC( "npc_turret_mega", TEAM_UNASSIGNED, info_target.GetOrigin(), info_target.GetAngles() )
+ SetSpawnOption_AISettings( turret, "npc_turret_mega_fortwar" )
+ DispatchSpawn( turret )
+
+ turretsite.turret = turret
+
+ // init turret settings
+ turret.s.minimapstate <- null // entity, for saving turret's minimap handler
+ turret.s.baseTurret <- false // bool, is this turret from base
+ turret.s.turretflagid <- "" // string, turret's id like "1", "2", "3"
+ turret.s.lastDamagedTime <- 0.0 // float, for showing turret underattack icons
+ turret.s.relatedBatteryPort <- null // entity, corssfile
+
+ // minimap icons holder
+ entity minimapstate = CreateEntity( "prop_script" )
+ minimapstate.SetValueForModelKey( info_target.GetModelName() ) // these info must have model to work
+ minimapstate.Hide() // hide the model! it will still work on minimaps
+ minimapstate.SetOrigin( info_target.GetOrigin() )
+ minimapstate.SetAngles( info_target.GetAngles() )
+ //SetTeam( minimapstate, info_target.GetTeam() ) // setTeam() for icons is done in TurretStateWatcher()
+ minimapstate.kv.solid = SOLID_VPHYSICS
+ DispatchSpawn( minimapstate )
+ // show on minimaps
+ minimapstate.Minimap_AlwaysShow( TEAM_IMC, null )
+ minimapstate.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ minimapstate.Minimap_SetCustomState( eMinimapObject_prop_script.FW_BUILDSITE_SHIELDED )
+
+ turretsite.minimapstate = minimapstate
+ turret.s.minimapstate = minimapstate
+
+ break
+ }
+ }
+ }
+
+ // script_ref
+ foreach ( entity script_ref in GetEntArrayByClass_Expensive( "script_ref" ) )
+ {
+ if( script_ref.HasKey( "editorclass" ) )
+ {
+ switch( script_ref.kv.editorclass )
+ {
+ case "info_fw_foundation_plate":
+ entity prop = CreatePropScript( script_ref.GetModelName(), script_ref.GetOrigin(), script_ref.GetAngles(), 6 )
+ break
+ case "info_fw_battery_port":
+ entity batteryPort = CreatePropScript( script_ref.GetModelName(), script_ref.GetOrigin(), script_ref.GetAngles(), 6 )
+ FW_InitBatteryPort(batteryPort)
+
+ break
+ }
+ }
+ }
+
+ // trigger_multiple
+ foreach ( entity trigger_multiple in GetEntArrayByClass_Expensive( "trigger_multiple" ) )
+ {
+ if( trigger_multiple.HasKey( "editorclass" ) )
+ {
+ switch( trigger_multiple.kv.editorclass )
+ {
+ case "trigger_fw_territory":
+ SetupFWTerritoryTrigger( trigger_multiple )
+ break
+ }
+ }
+ }
+
+ // maybe for tick_spawning reapers?
+ ValidateAndFinalizePendingStationaryPositions()
+}
+
+void function InitCampSpawnerLevel() // can edit this to make more spawns, alertLevel icons supports max to lv3( 0,1,2 )
+{
+ // lv1 spawns: grunts
+ CampSpawnStruct campSpawnLv1
+ campSpawnLv1.spawnContent = "npc_soldier"
+ campSpawnLv1.maxSpawnCount = FW_GRUNT_MAX_DEPLOYED
+ campSpawnLv1.countPerSpawn = 4 // how many npcs to deploy per spawn, for droppods most be 4
+ campSpawnLv1.killsToEscalate = FW_GRUNT_COUNT / 2
+
+ file.fwNpcLevel[0] <- campSpawnLv1
+
+ // lv2 spawns: spectres
+ CampSpawnStruct campSpawnLv2
+ campSpawnLv2.spawnContent = "npc_spectre"
+ campSpawnLv2.maxSpawnCount = FW_SPECTRE_MAX_DEPLOYED
+ campSpawnLv2.countPerSpawn = 4 // how many npcs to deploy per spawn, for droppods most be 4
+ campSpawnLv2.killsToEscalate = FW_SPECTRE_COUNT / 2
+
+ file.fwNpcLevel[1] <- campSpawnLv2
+
+ // lv3 spawns: reapers
+ CampSpawnStruct campSpawnLv3
+ campSpawnLv3.spawnContent = "npc_super_spectre"
+ campSpawnLv3.maxSpawnCount = FW_REAPER_MAX_DEPLOYED
+ campSpawnLv3.countPerSpawn = 1 // how many npcs to deploy per spawn
+ campSpawnLv3.killsToEscalate = FW_REAPER_COUNT / 2 // only 1 kill needed to spawn the boss?
+
+ file.fwNpcLevel[2] <- campSpawnLv3
+}
+
+/////////////////////////////////////////////
+///// GAMEMODE INITIALIZE FUNCTIONS END /////
+/////////////////////////////////////////////
+
+
+
+///////////////////////////////////////
+///// PLAYER INITIALIZE FUNCTIONS /////
+///////////////////////////////////////
+
+void function InitFWPlayers( entity player )
+{
+ HarvesterDamageStruct emptyStruct
+ file.playerDamageHarvester[ player ] <- emptyStruct
+
+ // objective stuff
+ player.s.notifiedTitanfall <- false
+
+ // notification stuff
+ player.s.lastTurretNotifyTime <- 0.0
+}
+
+///////////////////////////////////////////
+///// PLAYER INITIALIZE FUNCTIONS END /////
+///////////////////////////////////////////
+
+
+
+/////////////////////////////
+///// POWERUP FUNCTIONS /////
+/////////////////////////////
+
+void function FWAddPowerUpIcon( entity powerup )
+{
+ powerup.Minimap_SetAlignUpright( true )
+ powerup.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ powerup.Minimap_SetClampToEdge( false )
+ powerup.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ powerup.Minimap_AlwaysShow( TEAM_IMC, null )
+}
+
+/////////////////////////////////
+///// POWERUP FUNCTIONS END /////
+/////////////////////////////////
+
+
+
+/////////////////////////////
+///// AICAMPS FUNCTIONS /////
+/////////////////////////////
+
+void function InitFWCampSites()
+{
+ // init here
+ foreach( entity info_target in file.camps )
+ {
+ InitCampTracker( info_target )
+ }
+
+ // camps don't have a id, set them manually
+ foreach( int index, CampSiteStruct campsite in file.fwCampSites )
+ {
+ entity campInfo = campsite.camp
+ float radius = float( campInfo.kv.radius )
+
+ // get droppod spawns
+ foreach ( entity spawnpoint in SpawnPoints_GetDropPod() )
+ if ( Distance( campInfo.GetOrigin(), spawnpoint.GetOrigin() ) < radius )
+ campsite.validDropPodSpawns.append( spawnpoint )
+
+ // get titan spawns
+ foreach ( entity spawnpoint in SpawnPoints_GetTitan() )
+ if ( Distance( campInfo.GetOrigin(), spawnpoint.GetOrigin() ) < radius )
+ campsite.validTitanSpawns.append( spawnpoint )
+
+ if ( index == 0 )
+ {
+ campsite.campId = "A"
+ SetGlobalNetInt( "fwCampAlertA", 0 )
+ SetGlobalNetFloat( "fwCampStressA", 0.0 ) // start from empty
+ SetLocationTrackerID( campsite.tracker, 0 )
+ file.trackedCampNPCSpawns["A"] <- {}
+ continue
+ }
+ if ( index == 1 )
+ {
+ campsite.campId = "B"
+ SetGlobalNetInt( "fwCampAlertB", 0 )
+ SetGlobalNetFloat( "fwCampStressB", 0.0 ) // start from empty
+ SetLocationTrackerID( campsite.tracker, 1 )
+ file.trackedCampNPCSpawns["B"] <- {}
+ continue
+ }
+ if ( index == 2 )
+ {
+ campsite.campId = "C"
+ SetGlobalNetInt( "fwCampAlertC", 0 )
+ SetGlobalNetFloat( "fwCampStressC", 0.0 ) // start from empty
+ SetLocationTrackerID( campsite.tracker, 2 )
+ file.trackedCampNPCSpawns["C"] <- {}
+ continue
+ }
+ }
+}
+
+void function InitCampTracker( entity camp )
+{
+ //print("InitCampTracker")
+ CampSiteStruct campsite
+ campsite.camp = camp
+ file.fwCampSites.append( campsite )
+
+ entity placementHelper = CreateEntity( "info_placement_helper" )
+ placementHelper.SetOrigin( camp.GetOrigin() ) // tracker needs a owner to display
+ campsite.info = placementHelper
+ DispatchSpawn( placementHelper )
+
+ float radius = float( camp.kv.radius ) // radius to show up icon and spawn ais
+
+ entity tracker = GetAvailableCampLocationTracker()
+ tracker.SetOwner( placementHelper )
+ campsite.tracker = tracker
+ SetLocationTrackerRadius( tracker, radius )
+ DispatchSpawn( tracker )
+}
+
+void function StartFWCampThink()
+{
+ foreach( CampSiteStruct camp in file.fwCampSites )
+ {
+ //print( "has " + string( file.fwCampSites.len() ) + " camps in total" )
+ //print( "campId is " + camp.campId )
+ thread FWAiCampThink( camp )
+ }
+}
+
+// this is not using respawn's remaining code!
+void function FWAiCampThink( CampSiteStruct campsite )
+{
+ string campId = campsite.campId
+ string alertVarName = "fwCampAlert" + campId
+ string stressVarName = "fwCampStress" + campId
+
+
+ bool firstSpawn = true
+ while( GamePlayingOrSuddenDeath() )
+ {
+ wait WAVE_STATE_TRANSITION_TIME
+
+ int alertLevel = GetGlobalNetInt( alertVarName )
+ //print( "campsite" + campId + ".ignoredSinceLastClean: " + string( campsite.ignoredSinceLastClean ) )
+ if( campsite.ignoredSinceLastClean >= FW_CAMP_IGNORE_NEEDED && alertLevel > 0 ) // has been ignored many times, level > 0
+ alertLevel = 0 // reset level
+ else if( !firstSpawn ) // not the first spawn!
+ alertLevel += 1 // level up
+
+ if( alertLevel >= FW_MAX_LEVELS - 1 ) // reached max level?
+ alertLevel = FW_MAX_LEVELS - 1 // stay
+
+ // update netVars, don't know how client update these, sometimes they can't catch up
+ SetGlobalNetInt( alertVarName, alertLevel )
+ SetGlobalNetFloat( stressVarName, 1.0 ) // refill
+
+ // under attack, clean this
+ campsite.ignoredSinceLastClean = 0
+
+ CampSpawnStruct curSpawnStruct = file.fwNpcLevel[alertLevel]
+ string npcToSpawn = curSpawnStruct.spawnContent
+ int maxSpawnCount = curSpawnStruct.maxSpawnCount
+ int countPerSpawn = curSpawnStruct.countPerSpawn
+ int killsToEscalate = curSpawnStruct.killsToEscalate
+
+ // for this time's loop
+ file.trackedCampNPCSpawns[campId] = {}
+ int killsNeeded = killsToEscalate
+ int lastNpcLeft
+ while( true )
+ {
+ WaitFrame()
+
+ //print( alertVarName + " : " + string( GetGlobalNetInt( alertVarName ) ) )
+ //print( stressVarName + " : " + string( GetGlobalNetFloat( stressVarName ) ) )
+ //print( "campsite" + campId + ".ignoredSinceLastClean: " + string( campsite.ignoredSinceLastClean ) )
+
+ if( !( npcToSpawn in file.trackedCampNPCSpawns[campId] ) ) // init it
+ file.trackedCampNPCSpawns[campId][npcToSpawn] <- 0
+
+ int npcsLeft = file.trackedCampNPCSpawns[campId][npcToSpawn]
+ killsNeeded -= lastNpcLeft - npcsLeft
+
+ if( killsNeeded <= 0 ) // check if needs more kills
+ {
+ SetGlobalNetFloat( stressVarName, 0.0 ) // empty
+ AddIgnoredCountToOtherCamps( campsite )
+ break
+ }
+
+ // update stress bar
+ float campStressLeft = float( killsNeeded ) / float( killsToEscalate )
+ SetGlobalNetFloat( stressVarName, campStressLeft )
+ //print( "campStressLeft: " + string( campStressLeft ) )
+
+ if( maxSpawnCount - npcsLeft >= countPerSpawn && killsNeeded >= countPerSpawn ) // keep spawning
+ {
+ // spawn functions, for fw we only spawn one kind of enemy each time
+ // light units
+ if( npcToSpawn == "npc_soldier"
+ || npcToSpawn == "npc_spectre"
+ || npcToSpawn == "npc_stalker" )
+ thread FW_SpawnDroppodSquad( campsite, npcToSpawn )
+
+ // reapers
+ if( npcToSpawn == "npc_super_spectre" )
+ thread FW_SpawnReaper( campsite )
+
+ file.trackedCampNPCSpawns[campId][npcToSpawn] += countPerSpawn
+
+ // titans?
+ //else if( npcToSpawn == "npc_titan" )
+ //{
+ // file.trackedCampNPCSpawns[campId][npcToSpawn] += 4
+ //}
+ }
+
+ lastNpcLeft = file.trackedCampNPCSpawns[campId][npcToSpawn]
+ }
+
+ // first loop ends
+ firstSpawn = false
+ }
+}
+
+void function AddIgnoredCountToOtherCamps( CampSiteStruct senderCamp )
+{
+ foreach( CampSiteStruct camp in file.fwCampSites )
+ {
+ //print( "senderCampId is: " + senderCamp.campId )
+ //print( "curCampId is " + camp.campId )
+ if( camp.campId != senderCamp.campId ) // other camps
+ {
+ camp.ignoredSinceLastClean += 1
+ }
+ }
+}
+
+// functions from at
+void function FW_SpawnDroppodSquad( CampSiteStruct campsite, string aiType )
+{
+ entity spawnpoint
+ if ( campsite.validDropPodSpawns.len() == 0 )
+ spawnpoint = campsite.tracker // no spawnPoints valid, use camp itself to spawn
+ else
+ spawnpoint = campsite.validDropPodSpawns.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ AiGameModes_SpawnDropPod( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), FW_AI_TEAM, aiType, void function( array<entity> guys ) : ( campsite, aiType )
+ {
+ FW_HandleSquadSpawn( guys, campsite, aiType )
+ })
+}
+
+void function FW_HandleSquadSpawn( array<entity> guys, CampSiteStruct campsite, string aiType )
+{
+ foreach ( entity guy in guys )
+ {
+ guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) // NPC_ALLOW_INVESTIGATE is not allowed
+ guy.SetScriptName( FW_NPC_SCRIPTNAME ) // well no need
+ // show on minimap to let players kill them
+ guy.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ guy.Minimap_AlwaysShow( TEAM_IMC, null )
+
+ // untrack them on death
+ thread FW_WaitToUntrackNPC( guy, campsite.campId, aiType )
+ }
+ // at least don't let them running around
+ thread FW_ForceAssaultInCamp( guys, campsite.camp )
+}
+
+void function FW_SpawnReaper( CampSiteStruct campsite )
+{
+ entity spawnpoint
+ if ( campsite.validDropPodSpawns.len() == 0 )
+ spawnpoint = campsite.tracker // no spawnPoints valid, use camp itself to spawn
+ else
+ spawnpoint = campsite.validDropPodSpawns.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ AiGameModes_SpawnReaper( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), FW_AI_TEAM, "npc_super_spectre_aitdm",void function( entity reaper ) : ( campsite )
+ {
+ reaper.SetScriptName( FW_NPC_SCRIPTNAME ) // no neet rn
+ // show on minimap to let players kill them
+ reaper.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ reaper.Minimap_AlwaysShow( TEAM_IMC, null )
+
+ // at least don't let them running around
+ thread FW_ForceAssaultInCamp( [reaper], campsite.camp )
+ // untrack them on death
+ thread FW_WaitToUntrackNPC( reaper, campsite.campId, "npc_super_spectre" )
+ })
+}
+
+// maybe this will make them stay around the camp
+void function FW_ForceAssaultInCamp( array<entity> guys, entity camp )
+{
+ while( true )
+ {
+ bool oneGuyValid = false
+ foreach( entity guy in guys )
+ {
+ if( IsValid( guy ) )
+ {
+ guy.AssaultPoint( camp.GetOrigin() )
+ guy.AssaultSetGoalRadius( float( camp.kv.radius ) ) // the camp's radius
+ guy.AssaultSetFightRadius( 0 )
+ oneGuyValid = true
+ }
+ }
+ if( !oneGuyValid ) // no guys left
+ return
+
+ wait RandomFloatRange( 10, 15 ) // make randomness
+ }
+}
+
+void function FW_WaitToUntrackNPC( entity guy, string campId, string aiType )
+{
+ guy.WaitSignal( "OnDeath", "OnDestroy" )
+ if( aiType in file.trackedCampNPCSpawns[ campId ] ) // maybe escalated?
+ file.trackedCampNPCSpawns[ campId ][ aiType ]--
+}
+
+/////////////////////////////////
+///// AICAMPS FUNCTIONS END /////
+/////////////////////////////////
+
+
+
+///////////////////////////////
+///// TERRITORY FUNCTIONS /////
+///////////////////////////////
+
+void function SetupFWTerritoryTrigger( entity trigger )
+{
+ //print("trigger_fw_territory detected")
+ file.fwTerritories.append( trigger )
+ trigger.ConnectOutput( "OnStartTouch", EntityEnterFWTrig )
+ trigger.ConnectOutput( "OnEndTouch", EntityLeaveFWTrig )
+
+ // respawn didn't leave a key for trigger's team, let's set it manually.
+ if( Distance( trigger.GetOrigin(), file.harvesterMlt_info.GetOrigin() ) > Distance( trigger.GetOrigin(), file.harvesterImc_info.GetOrigin() ) )
+ SetTeam( trigger, TEAM_IMC )
+ else
+ SetTeam( trigger, TEAM_MILITIA )
+
+ // init
+ file.teamTerrLastConnectTime[ trigger.GetTeam() ] <- 0.0
+
+ thread FWTerritoryTriggerThink( trigger )
+}
+
+// since we're using a trigger_multiple, needs this to remove invalid keys
+void function FWTerritoryTriggerThink( entity trigger )
+{
+ trigger.EndSignal( "OnDestroy" )
+
+ while( true )
+ {
+ if( null in trigger.e.scriptTriggerData.entities )
+ delete trigger.e.scriptTriggerData.entities[ null ]
+ WaitFrame()
+ }
+}
+
+void function EntityEnterFWTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if( !IsValid( ent ) ) // post-spawns
+ return
+ if( !ent.IsPlayer() && !ent.IsTitan() ) // no neet to add props and grunts i guess
+ return
+ // functions that trigger_multiple missing
+ if( IsValid( ent ) )
+ {
+ ScriptTriggerAddEntity( trigger, ent )
+ thread ScriptTriggerPlayerDisconnectThink( trigger, ent )
+ //TryFWTerritoryDialogue( trigger, ent ) // WIP
+ file.teamTerrLastConnectTime[ trigger.GetTeam() ] = Time()
+ }
+
+ if( !IsValid(ent) )
+ return
+ if ( ent.IsPlayer() ) // notifications for player
+ {
+ MessageToPlayer( ent, eEventNotifications.Clear ) // clean up last message
+ bool sameTeam = ent.GetTeam() == trigger.GetTeam()
+ if ( sameTeam )
+ {
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyEnterFriendlyArea" )
+ ent.SetPlayerNetInt( "indicatorId", 1 ) // 1 means "FRIENDLY TERRITORY"
+ }
+ else
+ {
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyEnterEnemyArea" )
+ ent.SetPlayerNetInt( "indicatorId", 2 ) // 2 means "ENEMY TERRITORY"
+ }
+ }
+}
+
+void function EntityLeaveFWTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if( !IsValid( ent ) ) // post-spawns
+ return
+ if( !ent.IsPlayer() && !ent.IsTitan() ) // no neet to add props and grunts i guess
+ return
+ // functions that trigger_multiple missing
+ if( IsValid( ent ) )
+ {
+ if( ent in trigger.e.scriptTriggerData.entities ) // need to check this!
+ {
+ ScriptTriggerRemoveEntity( trigger, ent )
+ //TryFWTerritoryDialogue( trigger, ent ) // WIP
+ file.teamTerrLastConnectTime[ trigger.GetTeam() ] = Time()
+ }
+ }
+
+ if( !IsValid(ent) )
+ return
+ if ( ent.IsPlayer() ) // notifications for player
+ {
+ MessageToPlayer( ent, eEventNotifications.Clear ) // clean up
+ bool sameTeam = ent.GetTeam() == trigger.GetTeam()
+ if ( sameTeam )
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyExitFriendlyArea" )
+ else
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyExitEnemyArea" )
+ ent.SetPlayerNetInt( "indicatorId", 4 ) // 4 means "NO MAN'S LAND"
+ }
+}
+
+// globlized!
+bool function FW_IsPlayerInFriendlyTerritory( entity player )
+{
+ foreach( entity trigger in file.fwTerritories )
+ {
+ if( trigger.GetTeam() == player.GetTeam() ) // is it friendly one?
+ {
+ if( GetAllEntitiesInTrigger( trigger ).contains( player ) ) // is player inside?
+ return true
+ }
+ }
+ return false // can't find the player
+}
+
+// globlized!
+bool function FW_IsPlayerInEnemyTerritory( entity player )
+{
+ foreach( entity trigger in file.fwTerritories )
+ {
+ if( trigger.GetTeam() != player.GetTeam() ) // is it enemy one?
+ {
+ if( GetAllEntitiesInTrigger( trigger ).contains( player ) ) // is player inside?
+ return true
+ }
+ }
+ return false // can't find the player
+}
+
+///////////////////////////////////
+///// TERRITORY FUNCTIONS END /////
+///////////////////////////////////
+
+
+
+////////////////////////////////
+///// TITANSPAWN FUNCTIONS /////
+////////////////////////////////
+
+// territory trigger don't have a kv.radius, let's use a const
+// 2800 will pretty much get harvester's near titan startpoints
+const float FW_SPAWNPOINT_SEARCH_RADIUS = 2800.0
+
+
+Point function FW_ReCalculateTitanReplacementPoint( Point basePoint, entity player )
+{
+ int team = player.GetTeam()
+ // find team's harvester
+ entity teamHarvester = FW_GetTeamHarvesterProp( team )
+
+ if ( !IsValid( teamHarvester ) ) // team's havester has been destroyed!
+ return basePoint // return given value
+
+ if( Distance2D( basePoint.origin, teamHarvester.GetOrigin() ) <= FW_SPAWNPOINT_SEARCH_RADIUS ) // close enough!
+ return basePoint // this origin is good enough
+
+ // if not close enough to base, re-calculate
+ array<entity> fortWarPoints = FW_GetTitanSpawnPointsForTeam( team )
+ entity validPoint = GetClosest( fortWarPoints, basePoint.origin )
+ basePoint.origin = validPoint.GetOrigin()
+ return basePoint
+}
+
+bool function FW_RequestTitanAllowed( entity player, array< string > args )
+{
+ if( !FW_IsPlayerInFriendlyTerritory( player ) ) // is player in friendly base?
+ {
+ PlayFactionDialogueToPlayer( "tw_territoryNag", player ) // notify player
+ TryPlayTitanfallNegativeSoundToPlayer( player )
+ int objectiveID = 101 // which means "#FW_OBJECTIVE_TITANFALL"
+ Remote_CallFunction_NonReplay( player, "ServerCallback_FW_SetObjective", objectiveID )
+ return false
+ }
+ return true
+}
+
+bool function TryPlayTitanfallNegativeSoundToPlayer( entity player )
+{
+ if( !( "lastNegativeSound" in player.s ) )
+ player.s.lastNegativeSound <- 0.0 // float
+ if( player.s.lastNegativeSound + 1.0 > Time() ) // in sound cooldown
+ return false
+
+ // use a sound to notify player they can't titanfall here
+ EmitSoundOnEntityOnlyToPlayer( player, player, "titan_dryfire" )
+ player.s.lastNegativeSound = Time()
+
+ return true
+}
+
+array<entity> function FW_GetTitanSpawnPointsForTeam( int team )
+{
+ array<entity> validSpawnPoints
+ // find team's harvester
+ entity teamHarvester = FW_GetTeamHarvesterProp( team )
+
+ array<entity> allPoints
+ // same as _replacement_titans_drop.gnut does
+ allPoints.extend( GetEntArrayByClass_Expensive( "info_spawnpoint_titan" ) )
+ allPoints.extend( GetEntArrayByClass_Expensive( "info_spawnpoint_titan_start" ) )
+ allPoints.extend( GetEntArrayByClass_Expensive( "info_replacement_titan_spawn" ) )
+
+ // get valid points from all points
+ foreach( entity point in allPoints )
+ {
+ if( Distance2D( point.GetOrigin(), teamHarvester.GetOrigin() ) <= FW_SPAWNPOINT_SEARCH_RADIUS )
+ validSpawnPoints.append( point )
+ }
+
+ return validSpawnPoints
+}
+
+// some maps have reversed startpoints! we need a hack
+const array<string> TITAN_POINT_REVERSED_MAPS =
+[
+ "mp_grave"
+]
+
+// "Respawn as Titan" don't follow the rateSpawnPoints, fix it manually
+entity function FW_ForcedTitanStartPoint( entity player, entity basePoint )
+{
+ int team = player.GetTeam()
+ if ( TITAN_POINT_REVERSED_MAPS.contains( GetMapName() ) )
+ team = GetOtherTeam( player.GetTeam() )
+ array<entity> startPoints = SpawnPoints_GetTitanStart( team )
+ entity validPoint = startPoints[ RandomInt( startPoints.len() ) ] // choose a random( maybe not safe ) start point
+ return validPoint
+}
+
+////////////////////////////////////
+///// TITANSPAWN FUNCTIONS END /////
+////////////////////////////////////
+
+
+
+/////////////////////////////////
+///// THREATLEVEL FUNCTIONS /////
+/////////////////////////////////
+
+void function FWAreaThreatLevelThink()
+{
+ thread FWAreaThreatLevelThink_Threaded()
+}
+
+void function FWAreaThreatLevelThink_Threaded()
+{
+ entity imcTerritory
+ entity mltTerritory
+ foreach( entity territory in file.fwTerritories )
+ {
+ if( territory.GetTeam() == TEAM_IMC )
+ imcTerritory = territory
+ else
+ mltTerritory = territory
+ }
+
+ float lastWarningTime // for debounce
+ bool warnImcTitanApproach
+ bool warnMltTitanApproach
+ bool warnImcTitanInArea
+ bool warnMltTitanInArea
+
+ while( GamePlayingOrSuddenDeath() )
+ {
+ //print( " imc threat level is: " + string( GetGlobalNetInt( "imcTowerThreatLevel" ) ) )
+ //print( " mlt threat level is: " + string( GetGlobalNetInt( "milTowerThreatLevel" ) ) )
+ float imcLastDamage = fw_harvesterImc.lastDamage
+ float mltLastDamage = fw_harvesterMlt.lastDamage
+ bool imcShieldDown = fw_harvesterImc.harvesterShieldDown
+ bool mltShieldDown = fw_harvesterMlt.harvesterShieldDown
+
+ // imc threatLevel
+ if( imcLastDamage + FW_HARVESTER_DAMAGED_DEBOUNCE >= Time() && imcShieldDown )
+ SetGlobalNetInt( "imcTowerThreatLevel", 3 ) // 3 will show a "harvester being damaged" warning to player
+ else if( warnImcTitanInArea )
+ SetGlobalNetInt( "imcTowerThreatLevel", 2 ) // 2 will show a "titan in area" warning to player
+ else if( warnImcTitanApproach )
+ SetGlobalNetInt( "imcTowerThreatLevel", 1 ) // 1 will show a "titan approach" waning to player
+ else
+ SetGlobalNetInt( "imcTowerThreatLevel", 0 ) // 0 will hide all warnings
+
+ // militia threatLevel
+ if( mltLastDamage + FW_HARVESTER_DAMAGED_DEBOUNCE >= Time() && mltShieldDown )
+ SetGlobalNetInt( "milTowerThreatLevel", 3 ) // 3 will show a "harvester being damaged" warning to player
+ else if( warnMltTitanInArea )
+ SetGlobalNetInt( "milTowerThreatLevel", 2 ) // 2 will show a "titan in area" warning to player
+ else if( warnMltTitanApproach )
+ SetGlobalNetInt( "milTowerThreatLevel", 1 ) // 1 will show a "titan approach" waning to player
+ else
+ SetGlobalNetInt( "milTowerThreatLevel", 0 ) // 0 will hide all warnings
+
+
+ // clean it here
+ warnImcTitanInArea = false
+ warnMltTitanInArea = false
+ warnImcTitanApproach = false
+ warnMltTitanApproach = false
+
+ // get valid titans
+ array<entity> allTitans = GetNPCArrayByClass( "npc_titan" )
+ array<entity> allPlayers = GetPlayerArray()
+ foreach( entity player in allPlayers )
+ {
+ if( IsAlive( player ) && player.IsTitan() )
+ {
+ allTitans.append( player )
+ }
+ }
+
+ // check threats
+ array<entity> imcEntArray = GetAllEntitiesInTrigger( imcTerritory )
+ array<entity> mltEntArray = GetAllEntitiesInTrigger( mltTerritory )
+ imcEntArray.removebyvalue( null ) // since we're using a fake trigger, need to check this
+ mltEntArray.removebyvalue( null )
+ foreach( entity ent in imcEntArray )
+ {
+ //print( ent )
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.IsPlayer() || ent.IsNPC() )
+ {
+ if( ent.IsTitan() && ent.GetTeam() != TEAM_IMC )
+ warnImcTitanInArea = true
+ }
+ }
+ foreach( entity ent in mltEntArray )
+ {
+ //print( ent )
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.IsPlayer() || ent.IsNPC() )
+ {
+ if( ent.IsTitan() && ent.GetTeam() != TEAM_MILITIA )
+ warnMltTitanInArea = true
+ }
+ }
+
+ foreach( entity titan in allTitans )
+ {
+ if( !imcEntArray.contains( titan )
+ && !mltEntArray.contains( titan )
+ && titan.GetTeam() != TEAM_IMC
+ && !titan.e.isHotDropping )
+ warnImcTitanApproach = true // this titan must be in natural space
+
+ if( !mltEntArray.contains( titan )
+ && !imcEntArray.contains( titan )
+ && titan.GetTeam() != TEAM_MILITIA
+ && !titan.e.isHotDropping )
+ warnMltTitanApproach = true // this titan must be in natural space
+ }
+
+ WaitFrame()
+ }
+}
+
+/////////////////////////////////////
+///// THREATLEVEL FUNCTIONS END /////
+/////////////////////////////////////
+
+
+
+////////////////////////////
+///// TURRET FUNCTIONS /////
+////////////////////////////
+
+void function OnFWTurretSpawned( entity turret )
+{
+ turret.EnableTurret() // always enabled
+ SetDefaultMPEnemyHighlight( turret ) // for sonar highlights to work
+ AddEntityCallback_OnDamaged( turret, OnMegaTurretDamaged )
+ thread FWTurretHighlightThink( turret )
+}
+
+// this will clear turret's highlight upon their death, for notifying players to fix them
+void function FWTurretHighlightThink( entity turret )
+{
+ turret.EndSignal( "OnDestroy" )
+ WaitFrame() // wait a frame for other turret spawn options to set up
+ Highlight_SetFriendlyHighlight( turret, "fw_friendly" ) // initialize the highlight, they will show upon player's next respawn
+
+ turret.WaitSignal( "OnDeath" )
+ Highlight_ClearFriendlyHighlight( turret )
+}
+
+// for battery_port, replace the turret with new one
+entity function FW_ReplaceMegaTurret( entity perviousTurret )
+{
+ if( !IsValid( perviousTurret ) ) // previous turret not exist!
+ return
+
+ entity turret = CreateNPC( "npc_turret_mega", perviousTurret.GetTeam(), perviousTurret.GetOrigin(), perviousTurret.GetAngles() )
+ SetSpawnOption_AISettings( turret, "npc_turret_mega_fortwar" )
+ DispatchSpawn( turret )
+
+ // apply settings to new turret, must up on date
+ turret.s.baseTurret <- perviousTurret.s.baseTurret
+ turret.s.minimapstate <- perviousTurret.s.minimapstate
+ turret.s.turretflagid <- perviousTurret.s.turretflagid
+ turret.s.lastDamagedTime <- perviousTurret.s.lastDamagedTime
+ turret.s.relatedBatteryPort <- perviousTurret.s.relatedBatteryPort
+
+ int maxHealth = perviousTurret.GetMaxHealth()
+ int maxShield = perviousTurret.GetShieldHealthMax()
+ turret.SetMaxHealth( maxHealth )
+ turret.SetHealth( maxHealth )
+ turret.SetShieldHealth( maxShield )
+ turret.SetShieldHealthMax( maxShield )
+
+ // update turretSiteStruct
+ foreach( TurretSiteStruct turretsite in file.turretsites )
+ {
+ if( turretsite.turret == perviousTurret )
+ {
+ turretsite.turret = turret // only changed this
+ }
+ }
+
+ perviousTurret.Destroy() // destroy previous one
+
+ return turret
+}
+
+// avoid notifications overrides itself
+const float TURRET_NOTIFICATION_DEBOUNCE = 10.0
+
+void function OnMegaTurretDamaged( entity turret, var damageInfo )
+{
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ int scriptType = DamageInfo_GetCustomDamageType( damageInfo )
+ int turretTeam = turret.GetTeam()
+
+ if ( !damageSourceID && !damageAmount && !attacker )
+ return
+
+ if( turret.GetShieldHealth() - damageAmount <= 0 && scriptType != damageTypes.rodeoBatteryRemoval ) // this shot breaks shield
+ {
+ if ( !attacker.IsTitan() && !IsSuperSpectre( attacker ) )
+ {
+ if( attacker.IsPlayer() && attacker.GetTeam() != turret.GetTeam() ) // good to have
+ {
+ // avoid notifications overrides itself
+ if( attacker.s.lastTurretNotifyTime + TURRET_NOTIFICATION_DEBOUNCE < Time() )
+ {
+ MessageToPlayer( attacker, eEventNotifications.Clear ) // clean up last message
+ MessageToPlayer( attacker, eEventNotifications.TurretTitanDamageOnly )
+ attacker.s.lastTurretNotifyTime = Time()
+ }
+ }
+ DamageInfo_SetDamage( damageInfo, turret.GetShieldHealth() ) // destroy shields
+ return
+ }
+ }
+
+ // successfully damaged turret
+ turret.s.lastDamagedTime = Time()
+
+ if ( damageSourceID == eDamageSourceId.mp_titanweapon_heat_shield ||
+ damageSourceID == eDamageSourceId.mp_titanweapon_meteor_thermite ||
+ damageSourceID == eDamageSourceId.mp_titanweapon_flame_wall ||
+ damageSourceID == eDamageSourceId.mp_titanability_slow_trap ||
+ damageSourceID == eDamageSourceId.mp_titancore_flame_wave_secondary
+ ) // scorch's thermite damages
+ DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo )/2 ) // nerf scorch
+
+ // faction dialogue
+ damageAmount = DamageInfo_GetDamage( damageInfo )
+ if( turret.GetHealth() - damageAmount <= 0 ) // turret killed this shot
+ {
+ if( GamePlayingOrSuddenDeath() )
+ PlayFactionDialogueToTeam( "fortwar_turretDestroyedFriendly", turretTeam )
+ }
+}
+
+void function InitTurretSettings()
+{
+ foreach( TurretSiteStruct turretSite in file.turretsites )
+ {
+ entity turret = turretSite.turret
+ entity minimapstate = turretSite.minimapstate
+ int teamNum = turretSite.site.GetTeam()
+ int id = int( string( turretSite.site.kv.turretId ) )
+ string idString = string( id + 1 )
+ int team = int( string( turretSite.site.kv.teamnumber ) )
+
+ int stateFlag = 1 // netural
+
+ // spawn with teamNumber?
+ if( team == TEAM_IMC || team == TEAM_MILITIA )
+ turret.s.baseTurret = true
+
+ //SetTeam( minimapstate, team ) // setTeam() for icons is done in TurretStateWatcher()
+ SetTeam( turret, team )
+
+ //print( "Try to set globatNetEnt: " + "turretSite" + idString )
+
+ turret.s.turretflagid = idString
+ turretSite.turretflagid = idString
+
+ thread TurretStateWatcher( turretSite )
+ }
+}
+
+// about networkvar "turretStateFlags" value
+// 1 means destoryed/netural
+// 2 means imc turret
+// 4 means mlt turret
+// 10 means shielded imc turret
+// 13 means shielded mlt turret
+// 16 means destoryed/netural being attacked
+// 18 means imc turret being attacked
+// 20 means mlt turret being attacked
+// 26 means shielded imc turret being attacked
+// 28 means shielded mlt turret being attacked
+
+// unsure:
+// 24 means destroyed imc turret being attacked?
+// 40 means destroyed imc turret?
+// 48 means destroyed mlt turret being attacked?
+
+const int TURRET_DESTROYED_FLAG = 1
+const int TURRET_NATURAL_FLAG = 1
+const int TURRET_IMC_FLAG = 2
+const int TURRET_MLT_FLAG = 4
+const int TURRET_SHIELDED_IMC_FLAG = 10
+const int TURRET_SHIELDED_MLT_FLAG = 13
+
+const int TURRET_UNDERATTACK_NATURAL_FLAG = 16
+const int TURRET_UNDERATTACK_IMC_FLAG = 18
+const int TURRET_UNDERATTACK_MLT_FLAG = 20
+// natural turret noramlly can't get shielded
+const int TURRET_SHIELDED_UNDERATTACK_IMC_FLAG = 26
+const int TURRET_SHIELDED_UNDERATTACK_MLT_FLAG = 28
+
+void function TurretStateWatcher( TurretSiteStruct turretSite )
+{
+ entity mapIcon = turretSite.minimapstate
+ entity turret = turretSite.turret
+ entity batteryPort = expect entity( turret.s.relatedBatteryPort )
+
+ int turretHealth = GetCurrentPlaylistVarInt( "fw_turret_health", FW_DEFAULT_TURRET_HEALTH )
+ int turretShield = GetCurrentPlaylistVarInt( "fw_turret_shield", FW_DEFAULT_TURRET_SHIELD )
+ turret.SetMaxHealth( turretHealth )
+ turret.SetHealth( turretHealth )
+ turret.SetShieldHealthMax( turretShield )
+
+ string idString = turretSite.turretflagid
+ string siteVarName = "turretSite" + idString
+ string stateVarName = "turretStateFlags" + idString
+
+ // battery overlay icons holder
+ entity overlayState = CreateEntity( "prop_script" )
+ overlayState.SetValueForModelKey( $"models/communication/flag_base.mdl" ) // requires a model to show overlays
+ overlayState.Hide() // this can still show players overlay icons
+ overlayState.SetOrigin( batteryPort.GetOrigin() ) // tracking batteryPort's positions
+ overlayState.SetAngles( batteryPort.GetAngles() )
+ overlayState.kv.solid = SOLID_VPHYSICS
+ DispatchSpawn( overlayState )
+
+ svGlobal.levelEnt.EndSignal( "CleanUpEntitiesForRoundEnd" ) // end dialogues is good
+ mapIcon.EndSignal( "OnDestroy" ) // mapIcon should be valid all time, tracking it
+ batteryPort.EndSignal( "OnDestroy" ) // also track this
+ overlayState.EndSignal( "OnDestroy" )
+
+ SetGlobalNetEnt( siteVarName, overlayState ) // tracking batteryPort's positions and team
+ SetGlobalNetInt( stateVarName, TURRET_NATURAL_FLAG ) // init for all turrets
+
+ int lastFrameTeam
+ bool lastFrameIsAlive
+
+ while( true )
+ {
+ WaitFrame() // start of the loop
+
+ turret = turretSite.turret // need to keep updating, for sometimes it being replaced
+
+ if( !IsValid( turret ) ) // replacing turret this frame
+ continue // skip the loop once
+
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ int turretTeam = turret.GetTeam()
+ bool turretAlive = IsAlive( turret )
+
+ bool changedTeamThisFrame = lastFrameTeam != turretTeam // turret has changed team?
+ bool killedThisFrame = lastFrameIsAlive != turretAlive // turret has no health left?
+
+ if( !turretAlive ) // turret down, waiting to be repaired
+ {
+ if( !isBaseTurret ) // never reset base turret's team
+ {
+ SetTeam( turret, TEAM_UNASSIGNED )
+ SetTeam( mapIcon, TEAM_UNASSIGNED )
+ SetTeam( batteryPort, TEAM_UNASSIGNED )
+ SetTeam( overlayState, TEAM_UNASSIGNED )
+ batteryPort.SetUsableByGroup( "pilot" ) // show hints to any pilot
+ }
+ SetGlobalNetInt( stateVarName, TURRET_DESTROYED_FLAG )
+ continue
+ }
+
+ // wrong dialogue, it will say "The turret you requested is on the way"
+ //if( changedTeamThisFrame ) // has been hacked!
+ // PlayFactionDialogueToTeam( "fortwar_turretDeployFriendly", turretTeam )
+
+ int iconTeam = turretTeam == TEAM_BOTH ? TEAM_UNASSIGNED : turretTeam // specific check
+ SetTeam( mapIcon, iconTeam ) // update icon's team
+ SetTeam( batteryPort, turretTeam ) // update batteryPort's team
+ SetTeam( overlayState, iconTeam ) // update overlayEnt's team
+
+ if( turretTeam != TEAM_BOTH && turretTeam != TEAM_UNASSIGNED ) // not a natural turret nor dead
+ batteryPort.SetUsableByGroup( "friendlies pilot" ) // only show hint to friendlies
+
+ float lastDamagedTime = expect float( turret.s.lastDamagedTime )
+ int stateFlag = TURRET_NATURAL_FLAG
+
+ // imc states
+ if( iconTeam == TEAM_IMC )
+ {
+ if( lastDamagedTime + FW_TURRET_DAMAGED_DEBOUNCE >= Time() ) // recent underattack
+ {
+ if( turret.GetShieldHealth() > 0 ) // has shields
+ stateFlag = TURRET_SHIELDED_UNDERATTACK_IMC_FLAG
+ else
+ stateFlag = TURRET_UNDERATTACK_IMC_FLAG
+
+ // these dialogue have 30s debounce inside
+ if( isBaseTurret )
+ PlayFactionDialogueToTeam( "fortwar_baseTurretsUnderAttack", TEAM_IMC )
+ else
+ PlayFactionDialogueToTeam( "fortwar_awayTurretsUnderAttack", TEAM_IMC )
+ }
+ else if( turret.GetShieldHealth() > 0 ) // has shields left
+ stateFlag = TURRET_SHIELDED_IMC_FLAG
+ else
+ stateFlag = TURRET_IMC_FLAG
+ }
+
+ // mlt states
+ if( iconTeam == TEAM_MILITIA )
+ {
+ if( lastDamagedTime + FW_TURRET_DAMAGED_DEBOUNCE >= Time() ) // recent underattack
+ {
+ if( turret.GetShieldHealth() > 0 ) // has shields
+ stateFlag = TURRET_SHIELDED_UNDERATTACK_MLT_FLAG
+ else
+ stateFlag = TURRET_UNDERATTACK_MLT_FLAG
+
+ // these dialogue have 30s debounce inside
+ if( isBaseTurret )
+ PlayFactionDialogueToTeam( "fortwar_baseTurretsUnderAttack", TEAM_MILITIA )
+ else
+ PlayFactionDialogueToTeam( "fortwar_awayTurretsUnderAttack", TEAM_MILITIA )
+ }
+ else if( turret.GetShieldHealth() > 0 ) // has shields left
+ stateFlag = TURRET_SHIELDED_MLT_FLAG
+ else
+ stateFlag = TURRET_MLT_FLAG
+ }
+
+ // natural states
+ if( iconTeam == TEAM_UNASSIGNED )
+ {
+ if( lastDamagedTime + FW_TURRET_DAMAGED_DEBOUNCE >= Time() ) // recent underattack
+ stateFlag = TURRET_UNDERATTACK_NATURAL_FLAG
+ else
+ stateFlag = TURRET_NATURAL_FLAG
+ }
+
+ SetGlobalNetInt( stateVarName, stateFlag )
+
+ // update these
+ lastFrameTeam = turretTeam
+ lastFrameIsAlive = turretAlive
+
+ WaitFrame()
+ }
+}
+
+////////////////////////////////
+///// TURRET FUNCTIONS END /////
+////////////////////////////////
+
+
+
+///////////////////////////////
+///// HARVESTER FUNCTIONS /////
+///////////////////////////////
+
+void function startFWHarvester()
+{
+ thread HarvesterThink(fw_harvesterImc)
+ thread HarvesterAlarm(fw_harvesterImc)
+ thread UpdateHarvesterHealth( TEAM_IMC )
+
+ thread HarvesterThink(fw_harvesterMlt)
+ thread HarvesterAlarm(fw_harvesterMlt)
+ thread UpdateHarvesterHealth( TEAM_MILITIA )
+}
+
+entity function FW_GetTeamHarvesterProp( int team )
+{
+ if( team == TEAM_IMC )
+ return fw_harvesterImc.harvester
+ else if( team == TEAM_MILITIA )
+ return fw_harvesterMlt.harvester
+
+ unreachable // crash the game
+}
+
+void function FW_createHarvester()
+{
+ // imc havester spawn
+ fw_harvesterImc = SpawnHarvester( file.harvesterImc_info.GetOrigin(), file.harvesterImc_info.GetAngles(), GetCurrentPlaylistVarInt( "fw_harvester_health", FW_DEFAULT_HARVESTER_HEALTH ), GetCurrentPlaylistVarInt( "fw_harvester_shield", FW_DEFAULT_HARVESTER_SHIELD ), TEAM_IMC )
+ fw_harvesterImc.harvester.SetArmorType( ARMOR_TYPE_HEAVY )
+ fw_harvesterImc.harvester.Minimap_SetAlignUpright( true )
+ fw_harvesterImc.harvester.Minimap_AlwaysShow( TEAM_IMC, null )
+ fw_harvesterImc.harvester.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ fw_harvesterImc.harvester.Minimap_SetHeightTracking( true )
+ fw_harvesterImc.harvester.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ fw_harvesterImc.harvester.Minimap_SetCustomState( eMinimapObject_prop_script.FD_HARVESTER )
+ AddEntityCallback_OnFinalDamaged( fw_harvesterImc.harvester, OnHarvesterDamaged )
+ AddEntityCallback_OnPostDamaged( fw_harvesterImc.harvester, OnHarvesterPostDamaged )
+
+ // imc havester settings
+ // don't set this, or sonar pulse will try to find it and failed to set highlight
+ //fw_harvesterMlt.harvester.SetScriptName("fw_team_tower")
+ file.harvesters.append(fw_harvesterImc)
+ entity trackerImc = GetAvailableBaseLocationTracker()
+ trackerImc.SetOwner( fw_harvesterImc.harvester )
+ DispatchSpawn( trackerImc )
+ SetLocationTrackerRadius( trackerImc, 1 ) // whole map
+
+ // scores starts from 100, TeamScore means harvester health; TeamScore2 means shield bar
+ GameRules_SetTeamScore( TEAM_MILITIA , 100 )
+ GameRules_SetTeamScore2( TEAM_MILITIA , 100 )
+
+
+ // mlt havester spawn
+ fw_harvesterMlt = SpawnHarvester( file.harvesterMlt_info.GetOrigin(), file.harvesterMlt_info.GetAngles(), GetCurrentPlaylistVarInt( "fw_harvester_health", FW_DEFAULT_HARVESTER_HEALTH ), GetCurrentPlaylistVarInt( "fw_harvester_shield", FW_DEFAULT_HARVESTER_SHIELD ), TEAM_MILITIA )
+ fw_harvesterMlt.harvester.SetArmorType( ARMOR_TYPE_HEAVY )
+ fw_harvesterMlt.harvester.Minimap_SetAlignUpright( true )
+ fw_harvesterMlt.harvester.Minimap_AlwaysShow( TEAM_IMC, null )
+ fw_harvesterMlt.harvester.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ fw_harvesterMlt.harvester.Minimap_SetHeightTracking( true )
+ fw_harvesterMlt.harvester.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ fw_harvesterMlt.harvester.Minimap_SetCustomState( eMinimapObject_prop_script.FD_HARVESTER )
+ AddEntityCallback_OnFinalDamaged( fw_harvesterMlt.harvester, OnHarvesterDamaged )
+ AddEntityCallback_OnPostDamaged( fw_harvesterMlt.harvester, OnHarvesterPostDamaged )
+
+ // mlt havester settings
+ // don't set this, or sonar pulse will try to find it and failed to set highlight
+ //fw_harvesterImc.harvester.SetScriptName("fw_team_tower")
+ file.harvesters.append(fw_harvesterMlt)
+ entity trackerMlt = GetAvailableBaseLocationTracker()
+ trackerMlt.SetOwner( fw_harvesterMlt.harvester )
+ DispatchSpawn( trackerMlt )
+ SetLocationTrackerRadius( trackerMlt, 1 ) // whole map
+
+ // scores starts from 100, TeamScore means harvester health; TeamScore2 means shield bar
+ GameRules_SetTeamScore( TEAM_IMC , 100 )
+ GameRules_SetTeamScore2( TEAM_IMC , 100 )
+
+ InitHarvesterDamageMods()
+}
+
+void function FW_AddHarvesterDamageSourceModifier( int id, float mod )
+{
+ if ( !( id in file.harvesterDamageSourceMods ) )
+ file.harvesterDamageSourceMods[id] <- 1.0
+
+ file.harvesterDamageSourceMods[id] *= mod
+}
+
+void function FW_RemoveHarvesterDamageSourceModifier( int id, float mod )
+{
+ if ( !( id in file.harvesterDamageSourceMods ) )
+ return
+
+ file.harvesterDamageSourceMods[id] /= mod
+
+ if ( file.harvesterDamageSourceMods[id] == 1.0 )
+ delete file.harvesterDamageSourceMods[id]
+}
+
+void function InitHarvesterDamageMods()
+{
+ // Damage balancing
+ const float CORE_DAMAGE_FRAC = 0.67
+ const float NUKE_EJECT_DAMAGE_FRAC = 0.25
+ const float DOT_DAMAGE_FRAC = 0.5
+
+ // Core balancing
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_laser_cannon, CORE_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_salvo_core, CORE_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_flightcore_rockets, CORE_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_shift_core, CORE_DAMAGE_FRAC )
+ // Flame Core is not included since its single target damage is low compared to the others
+
+ // Nuke eject balancing
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.damagedef_nuclear_core, NUKE_EJECT_DAMAGE_FRAC )
+
+ // Damage over time balancing
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_dumbfire_rockets, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_meteor_thermite, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_flame_wall, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanability_slow_trap, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_flame_wave_secondary, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_heat_shield, DOT_DAMAGE_FRAC )
+}
+
+// this function can't handle specific damageSourceID, such as plasma railgun, but is the best to scale both shield and health damage
+void function OnHarvesterDamaged( entity harvester, var damageInfo )
+{
+ if ( !IsValid( harvester ) )
+ return
+
+ // Entities (non-Players and non-NPCs) don't consider damaged entity lists, which makes ground attacks (e.g. Arc Wave) and thermite hit more than they should
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( IsValid( inflictor ) && ( inflictor.e.onlyDamageEntitiesOnce || inflictor.e.onlyDamageEntitiesOncePerTick ) )
+ {
+ if ( inflictor.e.damagedEntities.contains( harvester ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ else
+ {
+ inflictor.e.damagedEntities.append( harvester )
+ }
+ }
+
+ int friendlyTeam = harvester.GetTeam()
+ int enemyTeam = GetOtherTeam( friendlyTeam )
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ HarvesterStruct harvesterstruct // current harveter's struct
+ if( friendlyTeam == TEAM_MILITIA )
+ harvesterstruct = fw_harvesterMlt
+ if( friendlyTeam == TEAM_IMC )
+ harvesterstruct = fw_harvesterImc
+
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ if((harvester.GetShieldHealth()-damageAmount)<0)
+ {
+ if( !harvesterstruct.harvesterShieldDown )
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownFriendly", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownEnemy", enemyTeam )
+ harvesterstruct.harvesterShieldDown = true // prevent shield dialogues from repeating
+ }
+ }
+
+ // always reset harvester's recharge delay
+ harvesterstruct.lastDamage = Time()
+
+ // Should be moved to a final damage callback once those are added
+ if ( damageSourceID in file.harvesterDamageSourceMods )
+ DamageInfo_ScaleDamage( damageInfo, file.harvesterDamageSourceMods[ damageSourceID ] )
+}
+void function OnHarvesterPostDamaged( entity harvester, var damageInfo )
+{
+ if ( !IsValid( harvester ) )
+ return
+
+ int friendlyTeam = harvester.GetTeam()
+ int enemyTeam = GetOtherTeam( friendlyTeam )
+
+ GameRules_SetTeamScore( friendlyTeam , 1.0 * GetHealthFrac( harvester ) * 100 )
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ int scriptType = DamageInfo_GetCustomDamageType( damageInfo )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+
+ if(damageAmount == 0)
+ return
+
+ if ( !damageSourceID && !damageAmount && !attacker ) // actually not dealing any damage?
+ return
+
+ // prevent player from sniping the harvester cross-map
+ if ( attacker.IsPlayer() && !FW_IsPlayerInEnemyTerritory( attacker ) )
+ {
+ Remote_CallFunction_NonReplay( attacker , "ServerCallback_FW_NotifyNeedsEnterEnemyArea" )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ DamageInfo_SetCustomDamageType( damageInfo, scriptType | DF_NO_INDICATOR ) // hide the hitmarker
+ return // these damage won't do anything to the harvester
+ }
+
+ HarvesterStruct harvesterstruct // current harveter's struct
+ if( friendlyTeam == TEAM_MILITIA )
+ harvesterstruct = fw_harvesterMlt
+ if( friendlyTeam == TEAM_IMC )
+ harvesterstruct = fw_harvesterImc
+
+
+ damageAmount = DamageInfo_GetDamage( damageInfo ) // get damageAmount again after all damage adjustments
+
+ if ( !attacker.IsTitan() )
+ {
+ if( attacker.IsPlayer() )
+ Remote_CallFunction_NonReplay( attacker , "ServerCallback_FW_NotifyTitanRequired" )
+ DamageInfo_SetDamage( damageInfo, harvester.GetShieldHealth() )
+ damageAmount = 0 // never damage haveter's prop
+ }
+
+ if( !harvesterstruct.harvesterShieldDown )
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownFriendly", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownEnemy", enemyTeam )
+ harvesterstruct.harvesterShieldDown = true // prevent shield dialogues from repeating
+ }
+
+ harvesterstruct.harvesterDamageTaken = harvesterstruct.harvesterDamageTaken + damageAmount // track damage for wave recaps
+ float newHealth = harvester.GetHealth() - damageAmount
+ float oldhealthpercent = ( ( harvester.GetHealth().tofloat() / harvester.GetMaxHealth() ) * 100 )
+ float healthpercent = ( ( newHealth / harvester.GetMaxHealth() ) * 100 )
+
+ if (healthpercent <= 75 && oldhealthpercent > 75) // we don't want the dialogue to keep saying "Harvester is below 75% health" everytime they take additional damage
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseDmgFriendly75", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseDmgEnemy75", enemyTeam )
+ }
+
+ if (healthpercent <= 50 && oldhealthpercent > 50)
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseDmgFriendly50", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseDmgEnemy50", enemyTeam )
+ }
+
+ if (healthpercent <= 25 && oldhealthpercent > 25)
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseDmgFriendly25", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseDmgEnemy25", enemyTeam )
+ }
+
+ if( newHealth <= 0 )
+ {
+ EmitSoundAtPosition(TEAM_UNASSIGNED,harvesterstruct.harvester.GetOrigin(),"coop_generator_destroyed")
+ newHealth = 0
+ harvesterstruct.rings.Destroy()
+ harvesterstruct.harvester.Dissolve( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+ }
+
+ harvester.SetHealth( newHealth )
+ harvesterstruct.havesterWasDamaged = true
+
+ if ( attacker.IsPlayer() )
+ {
+ // dialogue for enemy attackers
+ if( !harvesterstruct.harvesterShieldDown )
+ PlayFactionDialogueToTeam( "fortwar_baseEnemyAllyAttacking", enemyTeam )
+
+ attacker.NotifyDidDamage( harvester, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamagePosition( damageInfo ), DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageFlags( damageInfo ), DamageInfo_GetHitGroup( damageInfo ), DamageInfo_GetWeapon( damageInfo ), DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+
+ // get newest damage for adding score!
+ int scoreDamage = int( DamageInfo_GetDamage( damageInfo ) )
+ // score events
+ attacker.AddToPlayerGameStat( PGS_ASSAULT_SCORE, scoreDamage )
+
+ // add to player structs
+ file.playerDamageHarvester[ attacker ].recentDamageTime = Time()
+ file.playerDamageHarvester[ attacker ].storedDamage += scoreDamage
+
+ // enough to earn score?
+ if( file.playerDamageHarvester[ attacker ].storedDamage >= FW_HARVESTER_DAMAGE_SEGMENT )
+ {
+ AddPlayerScore( attacker, "FortWarTowerDamage", attacker )
+ attacker.AddToPlayerGameStat( PGS_DEFENSE_SCORE, POINTVALUE_FW_TOWER_DAMAGE )
+ file.playerDamageHarvester[ attacker ].storedDamage -= FW_HARVESTER_DAMAGE_SEGMENT // reset stored damage
+ }
+ }
+
+ // harvester down!
+ if ( harvester.GetHealth() == 0 )
+ {
+ // force deciding winner
+ SetWinner( enemyTeam )
+ //PlayFactionDialogueToTeam( "scoring_wonMercy", enemyTeam )
+ //PlayFactionDialogueToTeam( "fortwar_matchLoss", friendlyTeam )
+ GameRules_SetTeamScore2( friendlyTeam, 0 ) // force set score2 to 0( shield bar will empty )
+ GameRules_SetTeamScore( friendlyTeam, 0 ) // force set score to 0( health 0% )
+ }
+}
+
+void function HarvesterThink( HarvesterStruct fw_harvester )
+{
+ entity harvester = fw_harvester.harvester
+
+
+ EmitSoundOnEntity( harvester, "coop_generator_startup" )
+
+ float lastTime = Time()
+ wait 4
+ int lastShieldHealth = harvester.GetShieldHealth()
+ generateBeamFX( fw_harvester )
+ generateShieldFX( fw_harvester )
+
+ EmitSoundOnEntity( harvester, "coop_generator_ambient_healthy" )
+
+ bool isRegening = false // stops the regenning sound to keep stacking on top of each other
+
+ while ( IsAlive( harvester ) )
+ {
+ float currentTime = Time()
+ float deltaTime = currentTime -lastTime
+
+ if ( IsValid( fw_harvester.particleShield ) )
+ {
+ vector shieldColor = GetShieldTriLerpColor( 1.0 - ( harvester.GetShieldHealth().tofloat() / harvester.GetShieldHealthMax().tofloat() ) )
+ EffectSetControlPointVector( fw_harvester.particleShield, 1, shieldColor )
+ }
+
+ if( IsValid( fw_harvester.particleBeam ) )
+ {
+ vector beamColor = GetShieldTriLerpColor( 1.0 - ( harvester.GetHealth().tofloat() / harvester.GetMaxHealth().tofloat() ) )
+ EffectSetControlPointVector( fw_harvester.particleBeam, 1, beamColor )
+ }
+
+ if ( fw_harvester.harvester.GetShieldHealth() == 0 )
+ if( IsValid( fw_harvester.particleShield ) )
+ fw_harvester.particleShield.Destroy()
+
+ if ( ( ( currentTime-fw_harvester.lastDamage ) >= GetCurrentPlaylistVarFloat( "fw_harvester_regen_delay", FW_DEFAULT_HARVESTER_REGEN_DELAY ) ) && ( harvester.GetShieldHealth() < harvester.GetShieldHealthMax() ) )
+ {
+ if( !IsValid( fw_harvester.particleShield ) )
+ generateShieldFX( fw_harvester )
+
+ if( harvester.GetShieldHealth() == 0 )
+ EmitSoundOnEntity( harvester, "coop_generator_shieldrecharge_start" )
+
+ if (!isRegening)
+ {
+ EmitSoundOnEntity( harvester, "coop_generator_shieldrecharge_resume" )
+ fw_harvester.harvesterShieldDown = false
+ isRegening = true
+ }
+
+ float newShieldHealth = ( harvester.GetShieldHealthMax() / GetCurrentPlaylistVarFloat( "fw_harvester_regen_time", FW_DEFAULT_HARVESTER_REGEN_TIME ) * deltaTime ) + harvester.GetShieldHealth()
+
+ // shield full
+ if ( newShieldHealth >= harvester.GetShieldHealthMax() )
+ {
+ StopSoundOnEntity( harvester, "coop_generator_shieldrecharge_resume" )
+ harvester.SetShieldHealth( harvester.GetShieldHealthMax() )
+ EmitSoundOnEntity( harvester, "coop_generator_shieldrecharge_end" )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldUpFriendly", harvester.GetTeam() )
+ isRegening = false
+ }
+ else
+ {
+ harvester.SetShieldHealth( newShieldHealth )
+ }
+ }
+ else if ( ( ( currentTime-fw_harvester.lastDamage ) < GENERATOR_SHIELD_REGEN_DELAY ) && ( harvester.GetShieldHealth() < harvester.GetShieldHealthMax() ) )
+ {
+ isRegening = false
+ }
+
+ if ( ( lastShieldHealth > 0 ) && ( harvester.GetShieldHealth() == 0 ) )
+ {
+ EmitSoundOnEntity( harvester, "TitanWar_Harvester_ShieldDown" ) // add this
+ EmitSoundOnEntity( harvester, "coop_generator_shielddown" )
+ }
+
+ lastShieldHealth = harvester.GetShieldHealth()
+ lastTime = currentTime
+ WaitFrame()
+ }
+}
+
+void function HarvesterAlarm( HarvesterStruct fw_harvester )
+{
+ while( IsAlive( fw_harvester.harvester ) )
+ {
+ if( fw_harvester.harvester.GetShieldHealth() == 0 )
+ {
+ wait EmitSoundOnEntity( fw_harvester.harvester, "coop_generator_underattack_alarm" )
+ }
+ else
+ {
+ WaitFrame()
+ }
+ }
+}
+
+void function UpdateHarvesterHealth( int team )
+{
+ entity harvester
+ if( team == TEAM_MILITIA )
+ harvester = fw_harvesterMlt.harvester
+ if( team == TEAM_IMC )
+ harvester = fw_harvesterImc.harvester
+
+ while( true )
+ {
+ if( IsValid(harvester) )
+ {
+ GameRules_SetTeamScore2( team, 1.0 * harvester.GetShieldHealth() / harvester.GetShieldHealthMax() * 100 )
+ WaitFrame()
+ }
+ else // harvester down
+ {
+ int winnerTeam = GetOtherTeam(team)
+ SetWinner( winnerTeam )
+ //PlayFactionDialogueToTeam( "scoring_wonMercy", winnerTeam )
+ //PlayFactionDialogueToTeam( "fortwar_matchLoss", team )
+ GameRules_SetTeamScore2( team, 0 ) // force set score2 to 0( shield bar will empty )
+ GameRules_SetTeamScore( team, 0 ) // force set score to 0( health 0% )
+ break
+ }
+ }
+}
+
+///////////////////////////////////
+///// HARVESTER FUNCTIONS END /////
+///////////////////////////////////
+
+
+
+//////////////////////////////////////
+///// PLAYER OBJECTIVE FUNCTIONS /////
+//////////////////////////////////////
+
+const int APPLY_BATTERY_TEXT_INDEX = 96 // notify player to use batteries on turrets
+const int EARN_TITAN_TEXT_INDEX = 100 // notify player to earn titans
+const int CALL_IN_TITAN_TEXT_INDEX = 101 // notify player to call in titans in territory
+const int EMBARK_TITAN_TEXT_INDEX = 102 // notify player to embark titans
+const int ATTACK_HARVESTER_TEXT_INDEX = 103 // notify player to attack harvester
+
+void function FWPlayerObjectiveState()
+{
+ thread FWPlayerObjectiveState_Threaded()
+}
+
+void function FWPlayerObjectiveState_Threaded()
+{
+ while( GamePlayingOrSuddenDeath() )
+ {
+ foreach( player in GetPlayerArray() )
+ {
+ entity petTitan = player.GetPetTitan()
+ entity titanSoul
+ if( IsValid( petTitan ) )
+ titanSoul = petTitan.GetTitanSoul()
+
+ if ( IsValid( GetBatteryOnBack( player ) ) )
+ player.SetPlayerNetInt( "gameInfoStatusText", APPLY_BATTERY_TEXT_INDEX )
+ else if ( IsTitanAvailable( player ) )
+ {
+ if( !player.s.notifiedTitanfall ) // first notification, also do a objective announcement
+ {
+ SetObjective( player, CALL_IN_TITAN_TEXT_INDEX )
+ player.s.notifiedTitanfall = true
+ }
+ else
+ player.SetPlayerNetInt( "gameInfoStatusText", CALL_IN_TITAN_TEXT_INDEX )
+ }
+ else if ( IsValid( petTitan ) )
+ player.SetPlayerNetInt( "gameInfoStatusText", EMBARK_TITAN_TEXT_INDEX )
+ else if ( IsAlive( player ) && !player.IsTitan() )
+ player.SetPlayerNetInt( "gameInfoStatusText", EARN_TITAN_TEXT_INDEX )
+ else if( !IsValid( titanSoul ) ) // titan died or player first embarked
+ player.s.notifiedTitanfall = false
+
+ if ( !IsAlive( player ) ) // don't show objetive for dying players
+ player.SetPlayerNetInt( "gameInfoStatusText", -1 )
+ }
+ WaitFrame()
+ }
+
+ // game entered other state, clean this
+ foreach( player in GetPlayerArray() )
+ {
+ player.SetPlayerNetInt( "gameInfoStatusText", -1 )
+ }
+}
+
+void function SetObjective( entity player, int stringid )
+{
+ Remote_CallFunction_NonReplay( player, "ServerCallback_FW_SetObjective", stringid )
+ player.SetPlayerNetInt( "gameInfoStatusText", stringid )
+}
+
+void function SetTitanObjective( entity player, entity titan )
+{
+ SetObjective( player, ATTACK_HARVESTER_TEXT_INDEX )
+}
+
+void function SetPilotObjective( entity player, entity titan )
+{
+ if( titan.GetTitanSoul().IsEjecting() ) // this time titan is ejecting
+ {
+ SetObjective( player, EARN_TITAN_TEXT_INDEX )
+ player.s.notifiedTitanfall = false
+ }
+ else
+ player.SetPlayerNetInt( "gameInfoStatusText", EMBARK_TITAN_TEXT_INDEX )
+}
+
+//////////////////////////////////////////
+///// PLAYER OBJECTIVE FUNCTIONS END /////
+//////////////////////////////////////////
+
+
+
+/////////////////////////////////
+///// BatteryPort Functions /////
+/////////////////////////////////
+
+void function FW_InitBatteryPort( entity batteryPort )
+{
+ batteryPort.kv.fadedist = 10000 // try not to fade
+ InitTurretBatteryPort( batteryPort )
+
+ batteryPort.s.relatedTurret <- null // entity, for saving batteryPort's nearest turret
+
+ entity turret = GetNearestMegaTurret( batteryPort ) // consider this is the port's related turret
+
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ SetTeam( batteryPort, turret.GetTeam() )
+ batteryPort.s.relatedTurret = turret
+ batteryPort.s.isUsable <- FW_IsBatteryPortUsable
+ batteryPort.s.useBattery <- FW_UseBattery
+ if( isBaseTurret ) // this is a base turret!
+ {
+ batteryPort.s.hackAvaliable = false
+ batteryPort.SetUsableByGroup( "friendlies pilot" ) // only show hint to friendlies
+ } // it can never be hacked!
+
+ turret.s.relatedBatteryPort = batteryPort // do it here
+}
+
+function FW_IsBatteryPortUsable( batteryPortvar, playervar ) //actually bool function( entity, entity )
+{
+ entity batteryPort = expect entity( batteryPortvar )
+ entity player = expect entity( playervar )
+ entity turret = expect entity( batteryPort.s.relatedTurret )
+ if( !IsValid( turret ) ) // turret has been destroyed!
+ return false
+
+ // get turret's settings, decide behavior
+ bool validTeam = turret.GetTeam() == player.GetTeam() || turret.GetTeam() == TEAM_BOTH || turret.GetTeam() == TEAM_UNASSIGNED
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ // is this port able to be hacked
+ bool portHackAvaliable = expect bool( batteryPort.s.hackAvaliable )
+
+ // player has a battery, team valid or able to hack && not a base turret
+ return ( PlayerHasBattery( player ) && ( validTeam || ( portHackAvaliable && !isBaseTurret ) ) )
+}
+
+function FW_UseBattery( batteryPortvar, playervar ) //actually void function( entity, entity )
+{
+ entity batteryPort = expect entity( batteryPortvar )
+ entity player = expect entity( playervar )
+ // change turret settings
+ entity turret = expect entity( batteryPort.s.relatedTurret ) // consider this is the port's related turret
+
+ int playerTeam = player.GetTeam()
+ bool turretReplaced = false
+ bool sameTeam = turret.GetTeam() == player.GetTeam()
+
+ if( !IsAlive( turret ) ) // turret has been killed!
+ {
+ turret = FW_ReplaceMegaTurret( turret )
+ if( !IsValid( turret ) ) // replace failed!
+ return
+ batteryPort.s.relatedTurret = turret
+ turretReplaced = true // if turret has been replaced, mostly reset team!
+ }
+
+ bool teamChanged = false
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ if( ( !sameTeam || turretReplaced ) && !isBaseTurret ) // is there a need to change team?
+ {
+ SetTeam( turret, playerTeam )
+ teamChanged = true
+ }
+
+ // restore turret health
+ int newHealth = int ( min( turret.GetMaxHealth(), turret.GetHealth() + ( turret.GetMaxHealth() * GetCurrentPlaylistVarFloat( "fw_turret_fixed_health", TURRET_FIXED_HEALTH_PERCENTAGE ) ) ) )
+ if( turretReplaced || teamChanged ) // replaced/hacked turret will spawn with 50% health
+ newHealth = int ( turret.GetMaxHealth() * GetCurrentPlaylistVarFloat( "fw_turret_hacked_health", TURRET_HACKED_HEALTH_PERCENTAGE ) )
+ // restore turret shield
+ int newShield = int ( min( turret.GetShieldHealthMax(), turret.GetShieldHealth() + ( turret.GetShieldHealth() * GetCurrentPlaylistVarFloat( "fw_turret_fixed_shield", TURRET_FIXED_SHIELD_PERCENTAGE ) ) ) )
+ if( turretReplaced || teamChanged ) // replaced/hacked turret will spawn with 50% shield
+ newShield = int ( turret.GetShieldHealthMax() * GetCurrentPlaylistVarFloat( "fw_turret_hacked_shield", TURRET_HACKED_SHIELD_PERCENTAGE ) )
+ // only do team score event if turret's shields down, encourage players to hack more turrets
+ bool additionalScore = turret.GetShieldHealth() <= 0
+ // this can be too much powerful
+ turret.SetHealth( newHealth )
+ turret.SetShieldHealth( newShield )
+
+ // score event
+ string scoreEvent = "FortWarForwardConstruction"
+ int secondaryScore = POINTVALUE_FW_FORWARD_CONSTRUCTION
+ if( isBaseTurret ) // this is a base turret
+ {
+ scoreEvent = "FortWarBaseConstruction"
+ secondaryScore = POINTVALUE_FW_BASE_CONSTRUCTION
+ }
+ AddPlayerScore( player, scoreEvent, player ) // player themself gets more meter
+ player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, secondaryScore )
+
+ // only do team score event if turret's shields down
+ if( additionalScore )
+ {
+ // get turrets alive, for adding scores
+ string teamTurretCount = GetTeamAliveTurretCount_ReturnString( playerTeam )
+ foreach( entity friendly in GetPlayerArrayOfTeam( playerTeam ) )
+ AddPlayerScore( friendly, "FortWarTeamTurretControlBonus_" + teamTurretCount, friendly )
+
+ PlayFactionDialogueToTeam( "fortwar_turretShieldedByFriendlyPilot", playerTeam )
+ }
+
+}
+
+// get nearest turret, consider it belongs to the port
+entity function GetNearestMegaTurret( entity ent )
+{
+ array<entity> allTurrets = GetNPCArrayByClass( "npc_turret_mega" )
+ entity turret = GetClosest( allTurrets, ent.GetOrigin() )
+ return turret
+}
+
+// this will get english name of the count, since the "FortWarTeamTurretControlBonus_" score event uses it
+string function GetTeamAliveTurretCount_ReturnString( int team )
+{
+ int turretCount
+ foreach( entity turret in GetNPCArrayByClass( "npc_turret_mega" ) )
+ {
+ if( turret.GetTeam() == team && IsAlive( turret ) )
+ turretCount += 1
+ }
+
+ switch( turretCount )
+ {
+ case 1:
+ return "One"
+ case 2:
+ return "Two"
+ case 3:
+ return "Three"
+ case 4:
+ return "Four"
+ case 5:
+ return "Five"
+ case 6:
+ return "Six"
+ }
+
+ return ""
+}
+
+/////////////////////////////////////
+///// BatteryPort Functions End /////
+/////////////////////////////////////
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_gg.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_gg.gnut
index 8f34541b..ad46b42e 100644
--- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_gg.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_gg.gnut
@@ -18,14 +18,6 @@ void function GamemodeGG_Init()
AddCallback_GameStateEnter( eGameState.WinnerDetermined, OnWinnerDetermined )
AddCallback_GGEarnMeterFull( OnGGEarnMeterFilled )
-
- // set scorelimit if it's wrong, sort of a jank way to do it but best i've got rn
- try
- {
- if ( GetCurrentPlaylistVarInt( "scorelimit", GetGunGameWeapons().len() ) != GetGunGameWeapons().len() )
- SetPlaylistVarOverride( "scorelimit", GetGunGameWeapons().len().tostring() )
- }
- catch ( ex ) {}
}
void function OnPlayerDisconnected(entity player)
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut
index 4d52835b..6729ff97 100644
--- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_hidden.nut
@@ -1,5 +1,9 @@
global function GamemodeHidden_Init
+struct {
+ bool isVisible = false
+ array<entity> hiddens
+} file
void function GamemodeHidden_Init()
{
@@ -20,8 +24,7 @@ void function GamemodeHidden_Init()
AddCallback_GameStateEnter( eGameState.Postmatch, RemoveHidden )
SetTimeoutWinnerDecisionFunc( TimeoutCheckSurvivors )
- thread PredatorMain()
-
+ RegisterSignal( "VisibleNotification" )
}
void function HiddenInitPlayer( entity player )
@@ -78,7 +81,10 @@ void function MakePlayerHidden(entity player)
SetTeam( player, TEAM_IMC )
player.SetPlayerGameStat( PGS_ASSAULT_SCORE, 0 ) // reset kills
+ file.hiddens.append( player )
RespawnHidden( player )
+ thread PredatorMain( player )
+ thread VisibleNotification( player )
Remote_CallFunction_NonReplay( player, "ServerCallback_YouAreHidden" )
}
@@ -153,35 +159,66 @@ void function RemoveHidden()
}
}
-void function PredatorMain()
+void function PredatorMain( entity player )
{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+ float playerVel
+
while (true)
{
WaitFrame()
if(!IsLobby())
{
- foreach (entity player in GetPlayerArray())
+ if ( !IsValid( player ) || !IsAlive( player ) || player.GetTeam() != TEAM_IMC )
+ continue
+
+ vector playerVelV = player.GetVelocity()
+ playerVel = sqrt( playerVelV.x * playerVelV.x + playerVelV.y * playerVelV.y + playerVelV.z * playerVelV.z )
+
+ if ( playerVel/300 < 1.3 )
{
- if (player == null || !IsValid(player) || !IsAlive(player) || player.GetTeam() != TEAM_IMC)
- continue
- vector playerVelV = player.GetVelocity()
- float playerVel
- playerVel = sqrt(playerVelV.x * playerVelV.x + playerVelV.y * playerVelV.y + playerVelV.z * playerVelV.z)
- float playerVelNormal = playerVel * 0.068544
- if (playerVel/300 < 1.3)
+ player.SetCloakFlicker( 0, 0 )
+ player.kv.VisibilityFlags = 0
+ wait 0.5
+ if ( file.isVisible )
{
- player.SetCloakFlicker(0, 0)
- player.kv.VisibilityFlags = 0
- }
- else
- {
- player.SetCloakFlicker(0.2 , 1 )
- player.kv.VisibilityFlags = 0
- float waittime = RandomFloat(0.5)
- wait waittime
- player.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ file.isVisible = false
+ player.Signal( "VisibleNotification" )
}
}
+ else
+ {
+ player.SetCloakFlicker( 0.2 , 1 )
+ player.kv.VisibilityFlags = 0
+ float waittime = RandomFloat( 0.5 )
+ wait waittime
+ player.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ file.isVisible = true
+ }
+ }
+ }
+}
+
+void function VisibleNotification( entity player )
+{
+ player.EndSignal( "OnDeath" )
+ player.EndSignal( "OnDestroy" )
+ while (IsAlive(player))
+ {
+ WaitFrame()
+ if (!file.isVisible)
+ {
+ NSDeleteStatusMessageOnPlayer( player, "visibleTitle" )
+ NSDeleteStatusMessageOnPlayer( player, "visibleDesc" )
+ continue
+ }
+ else
+ {
+ NSCreateStatusMessageOnPlayer( player, "You are visible!", "", "visibleTitle" )
+ NSCreateStatusMessageOnPlayer( player, "Note:", "Slow down to remain invisible!", "visibleDesc" )
+ player.WaitSignal( "VisibleNotification" )
+ continue
}
}
}
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut
index 35e034cc..02f0799a 100644
--- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_inf.gnut
@@ -29,7 +29,7 @@ void function GamemodeInfection_Init()
void function InfectionInitPlayer( entity player )
{
- if ( GetGameState() < eGameState.Playing )
+ if ( GetGameState() < eGameState.Playing || !file.hasHadFirstInfection ) // per Gecko's suggestion, make anyone joining before first infected to stay as survivor instead
SetTeam( player, INFECTION_TEAM_SURVIVOR )
else
InfectPlayer( player )
@@ -45,7 +45,16 @@ void function SelectFirstInfectedDelayed()
wait 10.0 + RandomFloat( 5.0 )
array<entity> players = GetPlayerArray()
- entity infected = players[ RandomInt( players.len() ) ]
+
+ // End game if server empty on selecting infected
+ if ( !players.len() )
+ {
+ printt( "Couldn't select first infected: player array was empty" )
+ SetWinner( INFECTION_TEAM_SURVIVOR )
+ return
+ }
+
+ entity infected = players.getrandom()
InfectPlayer( infected )
RespawnInfected( infected )
@@ -185,6 +194,8 @@ void function SetLastSurvivor( entity player )
Remote_CallFunction_NonReplay( otherPlayer, "ServerCallback_AnnounceLastSurvivor", player.GetEncodedEHandle() )
Highlight_SetEnemyHighlight( player, "enemy_sonar" )
+ StatusEffect_AddEndless( player, eStatusEffect.sonar_detected, 1.0 ) // sonar is better here so the player themselves see the SONAR DETECTED warning.
+
if ( SpawnPoints_GetTitan().len() > 0 )
thread CreateTitanForPlayerAndHotdrop( player, GetTitanReplacementPoint( player, false ) )
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_tffa.gnut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_tffa.gnut
index 30aacad5..3b75e725 100644
--- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_tffa.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_tffa.gnut
@@ -63,6 +63,9 @@ void function PlayerWatchesTFFAIntroIntermissionCam( entity player )
wait TFFAIntroLength
+ if ( !IsValid( player ) ) // if player leaves during the intro sequence
+ return
+
RespawnAsTitan( player, false )
TryGameModeAnnouncement( player )
}
@@ -75,4 +78,4 @@ void function AddTeamScoreForPlayerKilled( entity victim, entity attacker, var d
// why isn't this PGS_SCORE? odd game
attacker.AddToPlayerGameStat( PGS_ASSAULT_SCORE, 1 )
}
-} \ No newline at end of file
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/cl_gamemode_fw_custom.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/cl_gamemode_fw_custom.nut
new file mode 100644
index 00000000..68a710e8
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/cl_gamemode_fw_custom.nut
@@ -0,0 +1,13 @@
+// cl_gamemode_fw already exists in vanilla game file
+// this file is to register more network vars or remote functions
+global function ServerCallback_FW_NotifyNeedsEnterEnemyArea
+
+void function ServerCallback_FW_NotifyNeedsEnterEnemyArea()
+{
+ AnnouncementData announcement = Announcement_Create( "#FW_ENTER_ENEMY_AREA" )
+ Announcement_SetSoundAlias( announcement, "UI_InGame_LevelUp" )
+ Announcement_SetSubText( announcement, "#FW_TITAN_REQUIRED_SUB" )
+ Announcement_SetPurge( announcement, true )
+ Announcement_SetPriority( announcement, 200 ) //Be higher priority than Titanfall ready indicator etc
+ AnnouncementFromClass( GetLocalViewPlayer(), announcement )
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut
index ca238d5d..c295d596 100644
--- a/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/sh_gamemode_fw_custom.nut
@@ -5,8 +5,39 @@
global function SHCreateGamemodeFW_Init
+// object settings, changable through playlist vars
+// default havester settings
+global const int FW_DEFAULT_HARVESTER_HEALTH = 87500
+global const int FW_DEFAULT_HARVESTER_SHIELD = 17500
+global const float FW_DEFAULT_HARVESTER_REGEN_DELAY = 12.0
+global const float FW_DEFAULT_HARVESTER_REGEN_TIME = 10.0
+// default turret settings
+global const int FW_DEFAULT_TURRET_HEALTH = 12500
+global const int FW_DEFAULT_TURRET_SHIELD = 4000
+
+// fix a turret
+global const float TURRET_FIXED_HEALTH_PERCENTAGE = 0.33
+global const float TURRET_FIXED_SHIELD_PERCENTAGE = 1.0 // default is regen all shield
+// hack a turret
+global const float TURRET_HACKED_HEALTH_PERCENTAGE = 0.5
+global const float TURRET_HACKED_SHIELD_PERCENTAGE = 0.5
+
void function SHCreateGamemodeFW_Init()
{
+ // harvester playlistvar
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_harvester_health", FW_DEFAULT_HARVESTER_HEALTH.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_harvester_shield", FW_DEFAULT_HARVESTER_SHIELD.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_harvester_regen_delay", FW_DEFAULT_HARVESTER_REGEN_DELAY.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_harvester_regen_time", FW_DEFAULT_HARVESTER_REGEN_TIME.tostring() )
+ // turret playlistvar
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_turret_health", FW_DEFAULT_TURRET_HEALTH.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_turret_shield", FW_DEFAULT_TURRET_SHIELD.tostring() )
+ // battery port playlistvar
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_turret_fixed_health", TURRET_FIXED_HEALTH_PERCENTAGE.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_turret_fixed_shield", TURRET_FIXED_SHIELD_PERCENTAGE.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_turret_hacked_health", TURRET_HACKED_HEALTH_PERCENTAGE.tostring() )
+ AddPrivateMatchModeSettingArbitrary( "#PL_fw", "fw_turret_hacked_shield", TURRET_HACKED_SHIELD_PERCENTAGE.tostring() )
+
AddCallback_OnCustomGamemodesInit( CreateGamemodeFW )
AddCallback_OnRegisteringCustomNetworkVars( FWOnRegisteringNetworkVars )
}
@@ -19,16 +50,28 @@ void function CreateGamemodeFW()
GameMode_Create( FORT_WAR )
GameMode_SetName( FORT_WAR, "#GAMEMODE_fw" )
GameMode_SetDesc( FORT_WAR, "#PL_fw_desc" )
- GameMode_SetGameModeAnnouncement( FORT_WAR, "ffa_modeDesc" ) // fw lines are unfortunately not registered to faction dialogue
+
+ // fw lines are unfortunately not registered to faction dialogue, maybe do it in gamemode script manually, current using it's modeName
+ GameMode_SetGameModeAnnouncement( FORT_WAR, "fortwar_modeName" )
- #if SERVER
- //GameMode_AddServerInit( FORT_WAR, GamemodeFW_Init ) // doesn't exist yet lol
- #elseif CLIENT
- GameMode_AddClientInit( FORT_WAR, CLGamemodeFW_Init )
- #endif
- #if !UI
- GameMode_AddSharedInit( FORT_WAR, SHGamemodeFW_Init )
- #endif
+ // waiting to be synced with client
+ GameMode_AddScoreboardColumnData( FORT_WAR, "#SCOREBOARD_KILLS", PGS_KILLS, 2 )
+ GameMode_AddScoreboardColumnData( FORT_WAR, "#SCOREBOARD_SUPPORT_SCORE", PGS_DEFENSE_SCORE, 4 )
+ GameMode_AddScoreboardColumnData( FORT_WAR, "#SCOREBOARD_COOP_POINTS", PGS_ASSAULT_SCORE, 6 )
+
+ AddPrivateMatchMode( FORT_WAR )
+
+ #if SERVER
+ GameMode_AddServerInit( FORT_WAR, GamemodeFW_Init )
+ GameMode_SetPilotSpawnpointsRatingFunc( FORT_WAR, RateSpawnpointsPilot_FW )
+ GameMode_SetTitanSpawnpointsRatingFunc( FORT_WAR, RateSpawnpointsTitan_FW )
+ #elseif CLIENT
+ GameMode_AddClientInit( FORT_WAR, CLGamemodeFW_Init )
+ #endif
+ #if !UI
+ GameMode_SetScoreCompareFunc( FORT_WAR, CompareAssaultScore )
+ GameMode_AddSharedInit( FORT_WAR, SHGamemodeFW_Init )
+ #endif
}
void function FWOnRegisteringNetworkVars()
@@ -36,6 +79,8 @@ void function FWOnRegisteringNetworkVars()
if ( GAMETYPE != FORT_WAR )
return
+ Remote_RegisterFunction( "ServerCallback_FW_NotifyNeedsEnterEnemyArea" )
+
RegisterNetworkedVariable( "turretSite1", SNDC_GLOBAL, SNVT_ENTITY )
RegisterNetworkedVariable( "turretSite2", SNDC_GLOBAL, SNVT_ENTITY )
RegisterNetworkedVariable( "turretSite3", SNDC_GLOBAL, SNVT_ENTITY )
@@ -59,13 +104,13 @@ void function FWOnRegisteringNetworkVars()
RegisterNetworkedVariable( "imcTowerThreatLevel", SNDC_GLOBAL, SNVT_INT )
RegisterNetworkedVariable( "milTowerThreatLevel", SNDC_GLOBAL, SNVT_INT )
RegisterNetworkedVariable( "fwCampAlertA", SNDC_GLOBAL, SNVT_INT )
- RegisterNetworkedVariable( "fwCampStressA", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "fwCampStressA", SNDC_GLOBAL, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
RegisterNetworkedVariable( "fwCampAlertB", SNDC_GLOBAL, SNVT_INT )
- RegisterNetworkedVariable( "fwCampStressB", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "fwCampStressB", SNDC_GLOBAL, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
RegisterNetworkedVariable( "fwCampAlertC", SNDC_GLOBAL, SNVT_INT )
- RegisterNetworkedVariable( "fwCampStressC", SNDC_GLOBAL, SNVT_INT )
+ RegisterNetworkedVariable( "fwCampStressC", SNDC_GLOBAL, SNVT_FLOAT_RANGE, 0.0, 0.0, 1.0 )
#if CLIENT
CLFortWar_RegisterNetworkFunctions()
#endif
-} \ No newline at end of file
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut
new file mode 100644
index 00000000..95ab3915
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/melee/sh_melee.gnut
@@ -0,0 +1,1226 @@
+untyped
+
+global function MeleeShared_Init
+
+global function CodeCallback_OnMeleePressed
+//global function CodeCallback_OnMeleeHeld
+//global function CodeCallback_OnMeleeReleased
+global function CodeCallback_IsValidMeleeExecutionTarget
+global function CodeCallback_IsValidMeleeAttackTarget
+global function CodeCallback_OnMeleeAttackAnimEvent
+global function AddSyncedMeleeServerCallback
+global function AddSyncedMeleeServerThink
+
+global function GetSyncedMeleeChooser
+global function CreateSyncedMeleeChooser
+global function PlayerTriesSyncedMelee
+global function FindBestSyncedMelee
+global function GetSyncedMeleeChooserForPlayerVsTarget
+global function AddSyncedMelee
+global function GetEyeOrigin
+global function SetObjectCanBeMeleed
+global function ObjectCanBeMeleed
+global function ShouldClampTargetVelocity
+global function ClampVerticalVelocity
+global function IsInExecutionMeleeState
+
+global function GetLungeTargetForPlayer
+global function Melee_IsAllowed
+global function IsAttackerRef
+global function AddCallback_IsValidMeleeExecutionTarget
+
+#if SERVER
+ global function Melee_Enable
+ global function Melee_Disable
+ global function SyncedMelee_Enable
+ global function SyncedMelee_Disable
+ global function InitMeleeAnimEventCallbacks
+ global function GetRefAnglesBetweenEnts
+ global function CreateMeleeScriptMoverBetweenEnts
+ global function ShouldHolsterWeaponForMelee
+ global function ShouldHolsterWeaponForSyncedMelee
+ global function NPCTriesSyncedMeleeVsPlayer
+#endif
+
+const SMOOTH_TIME = 0.2
+const INSTA_KILL_TIME_THRESHOLD = 0.35
+const BUG_REPRO_MOVEMELEE = 19114
+
+global struct SyncedMelee
+{
+ string ref
+ bool enabled = true
+ vector direction = < 1, 0, 0 >
+ float distance
+ float distanceSqr
+ string attackerAnimation1p
+ string attackerAnimation3p
+// void function AddAnimEvent( entity ent, string eventName, void functionref( entity ent ) func, var optionalVar = null )
+ array<AnimEventData> attacker3pAnimEvents
+ array<AnimEventData> target3pAnimEvents
+ string targetAnimation1p
+ string targetAnimation3p
+ string thirdPersonCameraAttachment
+ asset attachModel1p
+ string attachTag1p
+ float minDot = -1.0 // always happens
+ string animRefPos = "target"
+ bool canTargetNPCs = true
+ float percentDamageDealtPerHit = 1.0
+ bool usableByPlayers = true
+
+ float targetMinHealthRatio = 0.0 // target health ratio must be at least this high
+ float targetMaxHealthRatio = 1.0 // target health ratio must be at or below this
+ bool onlyIfLethal // only if the strike would be lethal
+ bool isAttackerRef = true
+
+}
+
+global struct SyncedMeleeChooser
+{
+ vector functionref( entity ) attackerOriginFunc
+ vector functionref( entity ) targetOriginFunc
+ array<SyncedMelee> syncedMelees
+ bool displayMeleePrompt = true
+}
+
+struct
+{
+ table<string, table<string,SyncedMeleeChooser> > syncedMeleeChoosers
+ table<SyncedMeleeChooser, array<void functionref( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target )> > syncedMeleeServerCallbacks
+ table<SyncedMeleeChooser, bool functionref( SyncedMelee action, entity player, entity target ) > syncedMeleeServerThink
+ array<bool functionref(entity attacker, entity target)> isValidMeleeExecutionTargetCallBacks
+ string lastExecutionUsed = ""
+} file
+
+function MeleeShared_Init()
+{
+ FlagInit( "ForceSyncedMelee" )
+
+ level.HUMAN_VS_TITAN_MELEE <- 1
+ level.titan_attack_anim_event_count <- 0
+ level.titan_attack_push_button_count <- 0
+
+ MeleeHumanShared_Init()
+ MeleeTitanShared_Init()
+ MeleeSyncedHumanShared_Init()
+ MeleeSyncedTitanShared_Init()
+
+ #if SERVER
+ VerifySyncedMelee()
+ MeleeSyncedServer_Init()
+ #endif
+
+ RegisterSignal( "SyncedMeleeComplete" )
+ RegisterSignal( "OnSyncedMelee" )
+ RegisterSignal( "OnSyncedMeleeVictim" )
+ RegisterSignal( "OnSyncedMeleeAttacker" )
+}
+
+int function GetPlayerMeleeDamage( entity player )
+{
+ Assert( player.IsPlayer() )
+ foreach ( weapon in player.GetMainWeapons() )
+ {
+ switch ( weapon.GetWeaponInfoFileKeyField( "fire_mode" ) )
+ {
+ case "offhand_melee":
+ return expect int( weapon.GetWeaponInfoFileKeyField( "melee_damage" ) )
+ }
+ }
+
+ return 0
+}
+
+void function AddSyncedMeleeServerCallback( SyncedMeleeChooser chooser, void functionref( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target ) func )
+{
+ if ( !( chooser in file.syncedMeleeServerCallbacks ) )
+ file.syncedMeleeServerCallbacks[ chooser ] <- []
+
+ file.syncedMeleeServerCallbacks[ chooser ].append( func )
+}
+
+void function AddSyncedMeleeServerThink( SyncedMeleeChooser chooser, bool functionref( SyncedMelee action, entity player, entity target ) func )
+{
+ file.syncedMeleeServerThink[ chooser ] <- func
+}
+
+
+function VerifySyncedMelee()
+{
+ foreach ( attackerChoosers in file.syncedMeleeChoosers )
+ {
+ foreach ( chooser in attackerChoosers )
+ {
+ //Assert( chooser in file.syncedMeleeServerCallbacks, "Need to add synced server melee callback for synced melee chooser" )
+ //Assert( file.syncedMeleeServerCallbacks[ chooser ].len() > 0, "Need to create a callback for chooser" )
+ Assert( chooser in file.syncedMeleeServerThink, "Need to add synced server melee callback for synced melee chooser" )
+ }
+ }
+}
+
+SyncedMeleeChooser function GetSyncedMeleeChooser( string attackerType, string victimType )
+{
+ return file.syncedMeleeChoosers[ attackerType ][ victimType ]
+}
+
+SyncedMeleeChooser function CreateSyncedMeleeChooser( string attackerType, string victimType )
+{
+ SyncedMeleeChooser chooser
+
+ chooser.attackerOriginFunc = GetEyeOrigin
+ chooser.targetOriginFunc = GetEyeOrigin
+
+ if ( !( attackerType in file.syncedMeleeChoosers ) )
+ file.syncedMeleeChoosers[ attackerType ] <- {}
+
+ Assert( !( victimType in file.syncedMeleeChoosers[ attackerType ] ), "Already has " + victimType )
+ file.syncedMeleeChoosers[ attackerType ][ victimType ] <- chooser
+ return chooser
+}
+
+vector function GetEyeOrigin( entity ent )
+{
+ return ent.EyePosition()
+}
+
+void function AddCallback_IsValidMeleeExecutionTarget( bool functionref( entity attacker, entity target ) callbackFunc )
+{
+ file.isValidMeleeExecutionTargetCallBacks.append( callbackFunc )
+}
+
+//Called after pressing the melee button to recheck for targets
+bool function CodeCallback_IsValidMeleeExecutionTarget( entity attacker, entity target )
+{
+ if ( attacker == target )
+ return false
+
+ if ( !ShouldPlayerExecuteTarget( attacker, target ) )
+ return false
+
+ if ( !attacker.IsOnGround() && attacker.IsHuman() )
+ return false
+
+ if ( !IsAlive( target ) )
+ return false
+
+ if ( target.IsInvulnerable() )
+ return false
+
+ if ( !CanBeMeleed( target ) )
+ return false
+
+ if ( target.IsNPC() && !target.CanBeMeleeExecuted() )
+ return false
+
+ // Disallow executing someone that is already in execution. That road leads to script errors and asserts.
+ if ( target.ContextAction_IsMeleeExecution() )
+ return false
+
+ if ( attacker.IsTitan() && target.IsTitan() )
+ {
+ // no melee execute for berserker
+ if ( PlayerHasPassive( attacker, ePassives.PAS_BERSERKER ) )
+ return false
+
+ if ( PlayerHasPassive( attacker, ePassives.PAS_SHIFT_CORE ) )
+ return false
+
+ if ( HasSoul( target ) && target.GetTitanSoul().IsEjecting() )
+ return false
+
+ if ( attacker.ContextAction_IsActive() )
+ return false
+
+ if ( target.ContextAction_IsActive() )
+ return false
+
+ if ( GetCurrentPlaylistVarInt( "vortex_blocks_melee", 0 ) == 1 )
+ {
+ vector traceStartPos = attacker.EyePosition()
+ vector traceEndPos = target.EyePosition()
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( attacker, traceStartPos, traceEndPos )
+ if ( vortexHit != null )
+ {
+ return false
+ }
+ }
+ }
+
+ if ( !CheckVerticallyCloseEnough( attacker, target ) )
+ return false
+
+ //No necksnaps while wall running or mantling
+ if ( attacker.IsWallRunning() )
+ return false
+
+ if ( attacker.IsTraversing() )
+ return false
+
+ if ( target.IsPlayer() ) //Disallow execution on a bunch of player-only actions
+ {
+
+ if ( target.IsHuman() )
+ {
+ if ( target.IsWallRunning() )
+ return false
+
+ if ( target.IsTraversing() )
+ return false
+
+ if ( !target.IsOnGround() ) //disallow mid-air necksnaps. Can't really do that for Titan executions since dash puts them in mid air... will have visual glitches unfortunately.
+ return false
+
+ if ( target.IsCrouched() )
+ return false
+
+ if ( Rodeo_IsAttached( target ) )
+ return false
+ }
+ }
+
+ if ( target.IsPhaseShifted() )
+ return false
+
+ //Disallow executions on contextActions marked Busy. Note that this allows
+ //execution on melee and leeching context actions!
+ if ( target.ContextAction_IsBusy() )
+ return false
+
+ if ( target.IsNPC() ) //NPC only checks
+ {
+ if ( target.ContextAction_IsActive() )
+ return false
+
+ if ( !target.IsInterruptable() )
+ return false
+ }
+
+ if ( attacker.GetTeam() == target.GetTeam() )
+ return false
+
+#if SERVER
+ if ( "syncedMeleeAttacker" in target.s ) //Don't allow necksnap on a guy who'se already getting necksnapped
+ return false
+#endif // #if SERVER
+
+ SyncedMeleeChooser ornull actions = GetSyncedMeleeChooserForPlayerVsTarget( attacker, target )
+ if ( actions == null )
+ return false
+ expect SyncedMeleeChooser( actions )
+
+ SyncedMelee ornull action = FindBestSyncedMelee( attacker, target, actions )
+ if ( action == null )
+ return false
+
+ if ( !PlayerMelee_IsExecutionReachable( attacker, target, 0.3 ) )
+ return false
+
+ foreach ( callbackFunc in file.isValidMeleeExecutionTargetCallBacks )
+ {
+ if ( !callbackFunc( attacker, target ) )
+ {
+ return false
+ }
+ }
+
+ return true
+}
+
+bool function CodeCallback_IsValidMeleeAttackTarget( entity attacker, entity target )
+{
+ if ( attacker == target )
+ return false
+
+ if ( target.IsBreakableGlass() )
+ return true
+
+ if ( !CanBeMeleed( target ) )
+ return false
+
+ if ( attacker.GetTeam() == target.GetTeam() )
+ return false
+
+#if SERVER
+ if ( target.IsPlayer() )
+ {
+ //Make titans not able to melee the pilot who is doing the embark animation
+ if ( GetTitanBeingRodeoed( target ) == attacker )
+ return false
+ }
+#endif // #if SERVER
+
+ if ( target.IsPhaseShifted() )
+ return false
+
+ if ( target.GetParent() == attacker )
+ return false
+
+ #if SERVER //Awkward, needed because it's CBaseCombatCharacter on server and C_BaseCombatCharacter on client, and because we allow melee on non BaseCombatCharacters like props that don't have ContextActions defined
+ if ( target instanceof CBaseCombatCharacter && target.ContextAction_IsMeleeExecutionTarget() ) //Don't lunge towards a victim that is already being executed )
+ return false
+ #elseif CLIENT
+ if ( target instanceof C_BaseCombatCharacter && target.ContextAction_IsMeleeExecutionTarget() ) //Don't lunge towards a victim that is already being executed )
+ return false
+ #endif
+
+ entity meleeWeapon = attacker.GetMeleeWeapon()
+ if ( !IsValid( meleeWeapon ) )
+ return false;
+
+ if ( !meleeWeapon.GetMeleeCanHitHumanSized() && IsHumanSized( target ) )
+ return false;
+ if ( !meleeWeapon.GetMeleeCanHitTitans() && target.IsTitan() )
+ return false;
+
+ return true
+}
+
+void function CodeCallback_OnMeleePressed( entity player )
+{
+#if SERVER
+ print( "SERVER: " + player + " pressed melee\n" )
+#else
+ print( "CLIENT: " + player + " pressed melee\n" )
+#endif
+
+ if ( !Melee_IsAllowed( player ) )
+ {
+#if SERVER
+ print( "SERVER: Melee_IsAllowed() for " + player + " is false\n" )
+#else
+ print( "CLIENT: Melee_IsAllowed() for " + player + " is false\n" )
+#endif
+ return
+ }
+
+#if SERVER
+ if ( svGlobal.cloakBreaksOnMelee && IsCloaked( player ) )
+ player.SetCloakFlicker( 1.0, 2.0 )
+#endif // #if SERVER
+
+ if ( player.IsWeaponDisabled() )
+ {
+#if SERVER
+ print( "SERVER: IsWeaponDisabled() for " + player + " is true\n" )
+#else
+ print( "CLIENT: IsWeaponDisabled() for " + player + " is true\n" )
+#endif
+ return
+ }
+
+ if ( player.PlayerMelee_GetState() != PLAYER_MELEE_STATE_NONE )
+ {
+#if SERVER
+ print( "SERVER: PlayerMelee_GetState() for " + player + " is " + player.PlayerMelee_GetState() + "\n" )
+#else
+ print( "CLIENT: PlayerMelee_GetState() for " + player + " is " + player.PlayerMelee_GetState() + "\n" )
+#endif
+ return
+ }
+
+ if ( !IsAlive( player ) )
+ {
+#if SERVER
+ print( "SERVER: " + player + " is dead\n" )
+#else
+ print( "CLIENT: " + player + " is dead\n" )
+#endif
+ return
+ }
+
+ thread CodeCallback_OnMeleePressed_InternalThread( player )
+}
+
+void function CodeCallback_OnMeleePressed_InternalThread( entity player )
+{
+ if ( player.IsTitan() )
+ {
+ TitanUnsyncedMelee( player )
+ }
+ else if ( player.IsHuman() )
+ {
+ const float STUN_EFFECT_CUTOFF = 0.05
+ float movestunEffect = StatusEffect_Get( player, eStatusEffect.move_slow )
+ bool movestunBlocked = (movestunEffect > STUN_EFFECT_CUTOFF)
+
+ HumanUnsyncedMelee( player, movestunBlocked )
+ }
+}
+
+//void function CodeCallback_OnMeleeHeld( entity player )
+//{
+//}
+
+//void function CodeCallback_OnMeleeReleased( entity player )
+//{
+//}
+
+bool function ShouldHolsterWeaponForSyncedMelee( entity player )
+{
+ if ( player.GetPlayerSettings() == "titan_ogre_minigun" )
+ return false
+
+ return ShouldHolsterWeaponForMelee( player )
+}
+
+bool function ShouldHolsterWeaponForMelee( entity player )
+{
+ #if !SERVER
+ return true
+ #endif
+
+ if ( !player.IsTitan() )
+ return true
+
+ return Time() - player.s.startDashMeleeTime > 1.0 //Fix issue with gun being out when it shouldn't, according to Mackey...
+}
+
+#if SERVER
+bool function NPCTriesSyncedMeleeVsPlayer( entity npc, entity player )
+{
+ Assert( npc.IsNPC() )
+ Assert( player.IsPlayer() )
+ Assert( IsAlive( player ) )
+ Assert( player.IsPlayer() )
+ Assert( IsPilot( player ) )
+ if ( player.ContextAction_IsBusy() )
+ return false
+
+ //#if SERVER
+ //player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION )
+ //#else
+ //player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION_PREDICTED )
+ //#endif
+
+ return DoSyncedMelee( npc, player )
+}
+#endif
+
+
+
+bool function PlayerTriesSyncedMelee( entity player, entity target )
+{
+ if ( !target )
+ return false
+ if ( !IsAlive( target ) )
+ return false
+
+ if ( target.ContextAction_IsBusy() )
+ return false
+
+ if ( player.IsTitan() )
+ {
+#if SERVER
+ player.PlayerMelee_SetState( PLAYER_MELEE_STATE_TITAN_EXECUTION )
+#else
+ player.PlayerMelee_SetState( PLAYER_MELEE_STATE_TITAN_EXECUTION_PREDICTED )
+#endif
+ }
+ else
+ {
+#if SERVER
+ player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION )
+#else
+ player.PlayerMelee_SetState( PLAYER_MELEE_STATE_HUMAN_EXECUTION_PREDICTED )
+#endif
+ }
+
+ if ( !player.Lunge_IsActive() || !player.Lunge_IsGroundExecute() || !player.Lunge_IsLungingToEntity() || (player.Lunge_GetTargetEntity() != target) )
+ {
+#if SERVER
+ print( "SERVER: " + player + " is calling Lunge_SetTargetEntity() from PlayerTriesSyncedMelee()\n" )
+#else
+ print( "CLIENT: " + player + " is calling Lunge_SetTargetEntity() from PlayerTriesSyncedMelee()\n" )
+#endif
+ player.Lunge_SetTargetEntity( target, false )
+ }
+
+#if SERVER
+ OnThreadEnd(
+ function() : ( player, target )
+ {
+ if ( IsValid( player ) && player.IsPlayer() )
+ {
+ RemoveCinematicFlag( player, CE_FLAG_TITAN_3P_CAM )
+ RemoveCinematicFlag( player, CE_FLAG_EXECUTION )
+ }
+ if ( IsValid( target ) && target.IsPlayer() )
+ {
+ RemoveCinematicFlag( target, CE_FLAG_TITAN_3P_CAM )
+ RemoveCinematicFlag( target, CE_FLAG_EXECUTION )
+ }
+ }
+ )
+
+ if ( player.IsTitan() )
+ TransferDamageHistoryToTarget( target )
+ if ( player.IsPlayer() )
+ {
+ AddCinematicFlag( player, CE_FLAG_TITAN_3P_CAM )
+ AddCinematicFlag( player, CE_FLAG_EXECUTION )
+ }
+ if ( IsValid( target ) && target.IsPlayer() )
+ {
+ AddCinematicFlag( target, CE_FLAG_TITAN_3P_CAM )
+ AddCinematicFlag( player, CE_FLAG_EXECUTION )
+ }
+#endif
+
+ bool success = DoSyncedMelee( player, target )
+ if ( !success )
+ {
+ player.Lunge_ClearTarget()
+ }
+
+ return success
+}
+
+function TransferDamageHistoryToTarget( entity target )
+{
+ entity titanSoul = target.GetTitanSoul()
+ target.e.recentDamageHistory = titanSoul.e.recentDamageHistory
+}
+
+bool function DoSyncedMelee( entity player, entity target )
+{
+ SyncedMeleeChooser ornull actions = GetSyncedMeleeChooserForPlayerVsTarget( player, target )
+
+ Assert( actions != null, "No melee action for " + player + " vs " + target )
+ expect SyncedMeleeChooser( actions )
+
+#if SERVER
+ if ( player.IsPlayer() )
+ {
+ PlayerMelee_StartLagCompensateTargetForLunge( player, target )
+ }
+#endif // #if SERVER
+
+ SyncedMelee ornull action = FindBestSyncedMelee( player, target, actions )
+
+#if SERVER
+ if ( player.IsPlayer() )
+ {
+
+ player.ForceStand()
+ player.UnforceStand()
+ PlayerMelee_FinishLagCompensateTarget( player )
+ }
+#endif // #if SERVER
+
+ if ( action == null )
+ return false
+
+ expect SyncedMelee( action )
+
+
+
+ player.Signal( "OnSyncedMelee" )
+ target.Signal( "OnSyncedMelee" )
+ player.Signal( "OnSyncedMeleeAttacker" )
+ target.Signal( "OnSyncedMeleeVictim" )
+
+#if SERVER
+ player.p.lastExecutionUsed = action.ref
+
+ if ( actions in file.syncedMeleeServerCallbacks )
+ thread SyncedMeleeServerCallbacks( actions, action, player, target )
+ bool functionref( SyncedMelee action, entity player, entity target ) think = file.syncedMeleeServerThink[ actions ]
+ return think( action, player, target )
+#endif // #if SERVER
+
+ return true
+}
+
+void function SyncedMeleeServerCallbacks( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target )
+{
+ // Added via AddSyncedMeleeServerCallback
+ foreach ( index, _ in file.syncedMeleeServerCallbacks[ actions ] )
+ {
+ void functionref( SyncedMeleeChooser actions, SyncedMelee action, entity player, entity target ) item = file.syncedMeleeServerCallbacks[ actions ][ index ]
+ item( actions, action, player, target )
+ }
+}
+
+/*
+void function CodeCallback_OnMeleeReleased( entity player )
+{
+}
+*/
+
+function TextDebug( string msg )
+{
+ wait 0.5
+ printt( msg )
+}
+
+bool function ShouldClampTargetVelocity( vector targetVelocity, vector pushBackVelocity, float clampRatio )
+{
+ float dot = DotProduct( targetVelocity, pushBackVelocity )
+ if ( dot < 0 )
+ return true
+
+ if ( dot <= 0 )
+ return false
+
+ float velRatio = LengthSqr( targetVelocity ) / LengthSqr( pushBackVelocity )
+
+ return velRatio < clampRatio
+}
+
+bool function CanBeMeleed( entity target )
+{
+ if ( target.IsPlayer() )
+ return true
+ if ( target.IsNPC() )
+ return true
+
+ if ( ObjectCanBeMeleed( target ) )
+ return true
+
+ return false
+}
+
+// IMPORTANT: Only used for non-player, non-living special cases like prop_dynamics we want to be able to melee (drones, etc)
+bool function ObjectCanBeMeleed( entity ent )
+{
+ if ( !( "canBeMeleed" in ent.s ) )
+ return false
+
+ return expect bool( ent.s.canBeMeleed )
+}
+
+// IMPORTANT: Only used for non-player, non-living special cases like prop_dynamics we want to be able to melee (drones, etc)
+function SetObjectCanBeMeleed( entity ent, bool value )
+{
+ Assert( !ent.IsPlayer(), ent + " should not be a player. This is for non-player, non-NPC entities.")
+ Assert( !ent.IsNPC(), ent + " should not be an NPC. This is for non-player, non-NPC entities.")
+
+ if ( !( "canBeMeleed" in ent.s ) )
+ ent.s.canBeMeleed <- false
+
+ ent.s.canBeMeleed = value
+}
+
+//function TitanExposionDeath( entity titan, entity attacker )
+//{
+// if ( !IsAlive( titan ) )
+// return
+//
+// ExplodeTitanBits( titan )
+// // and your pretty titan too!
+//
+// //TitanEjectExplosion
+// table deathTable = { scriptType = damageTypes.titanMelee, forceKill = true, damageType = DMG_MELEE_EXECUTION, damageSourceId = eDamageSourceId.titan_execution }
+// titan.TakeDamage( titan.GetMaxHealth() + 1, attacker, attacker, deathTable )
+//}
+
+#if SERVER
+vector function GetRefAnglesBetweenEnts( entity attacker, entity target )
+{
+ vector endOrigin = target.GetOrigin()
+ vector startOrigin = attacker.GetOrigin()
+ vector refVec = endOrigin - startOrigin
+ vector refAng = VectorToAngles( refVec )
+ if ( fabs( AngleNormalize( refAng.x ) ) > 35 ) //If pitch is too much, use angles from either attacker or target
+ {
+ if ( attacker.IsTitan() )
+ refAng = attacker.GetAngles() //Doing titan synced kill from front, so use attacker's origin
+ else
+ refAng = target.GetAngles() // Doing rear necksnap, so use target's angles
+ }
+ return refAng
+}
+
+entity function CreateMeleeScriptMoverBetweenEnts( entity attacker, entity target )
+{
+ vector refAng = GetRefAnglesBetweenEnts( attacker, target )
+
+ vector endOrigin = target.GetOrigin()
+ vector startOrigin = attacker.GetOrigin()
+ vector refVec = endOrigin - startOrigin
+ vector refPos = endOrigin - refVec * 0.5
+
+ entity ref = CreateOwnedScriptMover( attacker )
+ ref.SetOrigin( refPos )
+ ref.SetAngles( refAng )
+
+ return ref
+}
+#endif // SERVER
+
+void function AddSyncedMelee( SyncedMeleeChooser chooser, SyncedMelee melee )
+{
+ // sqr the distance
+ melee.distanceSqr = melee.distance * melee.distance
+
+ chooser.syncedMelees.append( melee )
+}
+
+SyncedMelee ornull function FindBestSyncedMelee( entity attacker, entity target, SyncedMeleeChooser actions )
+{
+ #if CLIENT
+ Assert( attacker == GetLocalViewPlayer() )
+ #endif // CLIENT
+
+ vector absTargetToPlayerDir
+ if ( attacker.IsPlayer() && attacker.Lunge_IsActive() && (attacker.Lunge_GetTargetEntity() == target) )
+ {
+ absTargetToPlayerDir = attacker.Lunge_GetStartPositionOffset()
+ absTargetToPlayerDir = Normalize( absTargetToPlayerDir )
+ }
+ else
+ {
+ vector attackerPos = actions.attackerOriginFunc( attacker ) // + ( attacker.GetVelocity() * SMOOTH_TIME )
+ vector targetPos = actions.targetOriginFunc( target )
+
+ if ( attackerPos == targetPos )
+ {
+ absTargetToPlayerDir = < 1, 0, 0 >
+ }
+ else
+ {
+ absTargetToPlayerDir = Normalize( attackerPos - targetPos )
+ }
+ }
+
+ vector angles = attacker.EyeAngles()
+ vector forward = AnglesToForward( angles )
+
+ vector relTargetToPlayerDir = CalcRelativeVector( < 0, target.EyeAngles().y, 0 >, absTargetToPlayerDir )
+
+ array<SyncedMelee> bestActions
+ float bestDot = -2.0
+ float distSqr = LengthSqr( actions.attackerOriginFunc( attacker ) - actions.targetOriginFunc( target ) )
+
+ bool npcTarget = target.IsNPC()
+ bool playerAttacker = attacker.IsPlayer()
+
+ int health = target.GetHealth()
+ float healthRatio = HealthRatio( target )
+ int meleeDamage
+ if ( attacker.IsNPC() )
+ {
+ meleeDamage = attacker.GetMeleeDamageMaxForTarget( target )
+ }
+ else if ( attacker.IsPlayer() )
+ {
+ meleeDamage = GetPlayerMeleeDamage( attacker )
+ }
+
+ SyncedMelee ornull returnVal = null
+
+#if MP
+ if ( IsPilot( attacker ) )
+ {
+ PilotLoadoutDef loadout = GetActivePilotLoadout( attacker )
+
+ foreach ( action in actions.syncedMelees )
+ {
+ if ( action.ref != loadout.execution )
+ continue
+
+ if ( npcTarget && !action.canTargetNPCs )
+ break
+
+ if ( playerAttacker && !action.usableByPlayers )
+ break
+
+ if ( healthRatio < action.targetMinHealthRatio )
+ break
+
+ if ( healthRatio > action.targetMaxHealthRatio )
+ break
+
+ if ( action.onlyIfLethal && health > meleeDamage )
+ break
+
+ if ( distSqr > action.distanceSqr )
+ break
+
+ float dot = relTargetToPlayerDir.Dot( action.direction )
+ if ( dot < action.minDot )
+ break
+
+#if SERVER
+ //Random Execution
+ if ( string( attacker.GetPersistentVar( "activePilotLoadout.execution" )) == "execution_random")
+ {
+ returnVal = PickRandomExecution(actions, attacker)
+ break
+ }
+#endif
+
+ returnVal = action
+ break
+ }
+ }
+ else
+ {
+#endif
+ foreach ( action in actions.syncedMelees )
+ {
+ if ( !action.enabled )
+ continue
+
+ if ( npcTarget && !action.canTargetNPCs )
+ continue
+
+ if ( playerAttacker && !action.usableByPlayers )
+ continue
+
+ if ( healthRatio < action.targetMinHealthRatio )
+ continue
+
+ if ( healthRatio > action.targetMaxHealthRatio )
+ continue
+
+ if ( action.onlyIfLethal && health > meleeDamage )
+ continue
+
+ if ( distSqr > action.distanceSqr )
+ continue
+
+ float dot = relTargetToPlayerDir.Dot( action.direction )
+
+ //printt( "Dot: " + dot )
+
+ if ( dot < action.minDot )
+ continue
+
+ if ( dot == bestDot )
+ {
+ bestActions.append( action )
+ continue
+ }
+
+ if ( dot > bestDot )
+ {
+ // found new best dot
+ bestActions.clear()
+ bestDot = dot
+ bestActions.append( action )
+ }
+ }
+
+ if ( bestActions.len() )
+ returnVal = bestActions.getrandom()
+#if MP
+ }
+#endif
+
+ return returnVal
+}
+
+string function GetAttackerSyncedMelee( entity ent )
+{
+ if ( ent.IsPlayer() )
+ {
+ // TODO: for MP, change this to be based on loadout choice
+ string bodyType = GetPlayerBodyType( ent )
+ if ( bodyType == "human" )
+ {
+ entity weapon = ent.GetActiveWeapon()
+ var weaponSyncedMelee
+
+ if ( IsValid( weapon ) )
+ weaponSyncedMelee = weapon.GetWeaponInfoFileKeyField( "synced_melee_action" )
+
+ if ( weaponSyncedMelee )
+ return string( weaponSyncedMelee )
+ }
+
+ return bodyType
+
+ }
+ else if ( IsProwler( ent ) )
+ {
+ return "prowler"
+ }
+ else if ( IsPilotElite( ent ) )
+ {
+ return "pilotelite"
+ }
+ else if ( IsSpectre( ent ) )
+ {
+ return "spectre"
+ }
+ else if ( ent.IsNPC() )
+ {
+ return ent.GetBodyType()
+ }
+ else if ( ent.IsTitan() )
+ {
+ return "titan"
+ }
+
+ unreachable
+}
+
+string function GetVictimSyncedMeleeTargetType( entity ent )
+{
+ string targetType
+
+ if ( ent.IsPlayer() && GetPlayerBodyType( ent ) == "human" )
+ {
+ targetType = "human"
+ }
+ else if ( IsProwler( ent ) )
+ {
+ targetType = "prowler"
+ }
+ else if ( IsPilotElite( ent ) )
+ {
+ targetType = "pilotelite"
+ }
+ else if ( ent.IsNPC() )
+ {
+ targetType = ent.GetBodyType()
+ }
+ else if ( ent.IsTitan() )
+ {
+ targetType = "titan"
+ }
+ else
+ {
+ Assert( 0, "Unknown ent type" )
+ }
+
+ return targetType
+}
+
+SyncedMeleeChooser ornull function GetSyncedMeleeChooserForPlayerVsTarget( entity attacker, entity target )
+{
+ string attackerType = GetAttackerSyncedMelee( attacker )
+ string targetType = GetVictimSyncedMeleeTargetType( target )
+
+ if ( !( attackerType in file.syncedMeleeChoosers ) )
+ return null
+
+ if ( !( targetType in file.syncedMeleeChoosers[ attackerType ] ) )
+ return null
+
+ return file.syncedMeleeChoosers[ attackerType ][ targetType ]
+}
+
+void function CodeCallback_OnMeleeAttackAnimEvent( entity player )
+{
+ Assert( IsValid( player ) )
+#if SERVER
+ print( "SERVER: " + player + " is calling CodeCallback_OnMeleeAttackAnimEvent()\n" )
+#else
+ print( "CLIENT: " + player + " is calling CodeCallback_OnMeleeAttackAnimEvent()\n" )
+#endif
+ if ( player.PlayerMelee_IsAttackActive() )
+ {
+ if ( player.IsTitan() )
+ TitanMeleeAttack( player )
+ else if ( player.IsHuman() )
+ HumanMeleeAttack( player )
+ }
+}
+
+bool function IsInExecutionMeleeState( entity player )
+{
+ local meleeState = player.PlayerMelee_GetState()
+ switch ( meleeState )
+ {
+ case PLAYER_MELEE_STATE_HUMAN_EXECUTION_PREDICTED:
+ case PLAYER_MELEE_STATE_HUMAN_EXECUTION:
+ case PLAYER_MELEE_STATE_TITAN_EXECUTION_PREDICTED:
+ case PLAYER_MELEE_STATE_TITAN_EXECUTION:
+ return true
+
+ default:
+ return false
+ }
+
+ unreachable
+}
+
+#if SERVER
+void function InitMeleeAnimEventCallbacks( entity player )
+{
+ AddAnimEvent( player, "screen_blackout", MeleeBlackoutScreen_AE )
+}
+
+void function MeleeBlackoutScreen_AE( entity player )
+{
+ ScreenFadeToBlack( player, 0.7, 1.2 )
+}
+#endif
+
+bool function ShouldPlayerExecuteTarget( entity player, entity target )
+{
+ if ( player.IsTitan() )
+ {
+ if ( !target.IsTitan() )
+ return false
+
+ if ( Flag( "ForceSyncedMelee" ) )
+ return true
+
+ if ( !GetDoomedState( target ) )
+ return false
+
+ entity soul = target.GetTitanSoul()
+ if ( soul != null )
+ {
+ if ( soul.GetShieldHealth() > 0 && GetCurrentPlaylistVarInt( "titan_shield_blocks_execution", 0 ) != 0 )
+ return false
+ }
+
+ if ( !SyncedMelee_IsAllowed( player ) )
+ return false
+
+ return true
+ }
+
+ if ( player.IsHuman() )
+ {
+ if ( !IsHumanSized( target ) )
+ return false
+
+#if SERVER
+ if ( Flag( "ForceSyncedMelee" ) )
+ return true
+#endif // #if SERVER
+
+ if ( !SyncedMelee_IsAllowed( player ) )
+ return false
+ }
+
+ return true
+}
+
+vector function ClampVerticalVelocity( vector targetVelocity, float maxVerticalVelocity )
+{
+ vector clampedVelocity = targetVelocity
+ if ( clampedVelocity.z > maxVerticalVelocity )
+ {
+ printt( "clampedVelocity.z: " + clampedVelocity.z +", maxVerticalVelocity:" + maxVerticalVelocity )
+ clampedVelocity = Vector( targetVelocity.x, targetVelocity.y, maxVerticalVelocity )
+ }
+
+ return clampedVelocity
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool function CheckVerticallyCloseEnough( entity attacker, entity target )
+{
+ vector attackerOrigin = attacker.GetOrigin()
+ vector targetOrigin = target.GetOrigin()
+
+ float verticalDistance = fabs( attackerOrigin.z - targetOrigin.z )
+ float halfHeight = 0
+
+ if ( attacker.IsTitan() )
+ halfHeight = 92.5
+ else if ( attacker.IsHuman() )
+ halfHeight = 30
+
+ Assert( halfHeight, "Attacker is neither Titan nor Human" )
+
+ //printt( "vertical distance: " + verticalDistance )
+ return verticalDistance < halfHeight
+}
+
+
+entity function GetLungeTargetForPlayer( entity player )
+{
+ // Titan melee does not lunge
+ if ( player.IsTitan() )
+ return null
+
+ if ( player.IsPhaseShifted() )
+ return null
+
+ entity lungeTarget = PlayerMelee_LungeConeTrace( player, SHARED_CB_IS_VALID_MELEE_ATTACK_TARGET )
+ return lungeTarget
+}
+
+#if SERVER
+void function Melee_Enable( entity player )
+{
+ player.SetPlayerNetBool( "playerAllowedToMelee", true )
+}
+
+void function Melee_Disable( entity player )
+{
+ player.SetPlayerNetBool( "playerAllowedToMelee", false )
+}
+
+void function SyncedMelee_Enable( entity player )
+{
+ player.SetPlayerNetBool( "playerAllowedToSyncedMelee", true )
+}
+
+void function SyncedMelee_Disable( entity player )
+{
+ player.SetPlayerNetBool( "playerAllowedToSyncedMelee", false )
+}
+#endif
+
+bool function Melee_IsAllowed( entity player )
+{
+ return player.GetPlayerNetBool( "playerAllowedToMelee" )
+}
+
+bool function SyncedMelee_IsAllowed( entity player )
+{
+ return player.GetPlayerNetBool( "playerAllowedToSyncedMelee" )
+}
+
+bool function IsAttackerRef( SyncedMelee ornull action, entity target )
+{
+ if ( action != null )
+ {
+ expect SyncedMelee( action )
+ if ( action.isAttackerRef )
+ {
+ return true
+ }
+ }
+
+ if ( !target )
+ return true
+
+ if ( !IsValid( target ) )
+ return true
+
+ if ( !target.IsPlayer() )
+ return true
+
+ return false
+}
+
+#if MP
+#if SERVER
+SyncedMelee ornull function PickRandomExecution( SyncedMeleeChooser actions, entity attacker )
+{
+ array<SyncedMelee> possibleExecutions = []
+
+ SyncedMelee neckSnap
+
+ foreach ( action in actions.syncedMelees )
+ {
+ if (action.ref == "execution_neck_snap")
+ neckSnap = action
+
+ if(!IsItemLocked( attacker, action.ref ) && action.ref != "execution_random" && action.ref != attacker.p.lastExecutionUsed)
+
+ possibleExecutions.append(action)
+ }
+
+ if (possibleExecutions.len() == 0)
+ return neckSnap
+
+ possibleExecutions.randomize()
+
+ return possibleExecutions[0]
+}
+#endif
+#endif \ No newline at end of file
diff --git a/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut b/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut
index ad433ae2..9aa86a43 100644
--- a/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/rodeo/_rodeo_titan.gnut
@@ -41,6 +41,9 @@ global function Battery_StopFXAndHideIconForPlayer
global function RemovePlayerAirControl //This function should really be in a server only SP & MP utility script file. No such file exists as of right now.
global function RestorePlayerAirControl //This function should really be in a server only SP & MP utility script file. No such file exists as of right now.
+// fort war needs these
+global function Rodeo_TakeBatteryAwayFromPilot
+
#if DEV
global function SetDebugRodeoPrint
global function GetDebugRodeoPrint
diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_damage_types.nut b/Northstar.Custom/mod/scripts/vscripts/sh_damage_types.nut
new file mode 100644
index 00000000..bae0116e
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/sh_damage_types.nut
@@ -0,0 +1,778 @@
+global function DamageTypes_Init
+global function RegisterWeaponDamageSourceName
+global function GetObitFromDamageSourceID
+global function DamageSourceIDToString
+global function DamageSourceIDHasString
+
+#if SERVER
+global function RegisterWeaponDamageSource
+global function RegisterWeaponDamageSources
+#endif
+
+struct
+{
+ table<int,string> damageSourceIDToName
+ table<int,string> damageSourceIDToString
+
+ // For new, modded damageSourceIDs.
+ // Holds triplets of [id, enum_name, display name]. Stored with no separation for ease of string conversion.
+ array<string> customDamageSourceIDList
+} file
+
+// For sending custom damage source IDs to clients
+const int SOURCE_ID_MAX_MESSAGE_LENGTH = 200 // JFS - Used to break messages sent to client into chunks in case it would hit the limitation on command argument length
+const string MESSAGE_SPACE_PADDING = "\xA6" // The "broken pipe" character. Trash character used to replace spaces in display name to allow sending via commands (args are separated by spaces).
+
+global enum eDamageSourceId
+{
+ invalid = -1 // used in code
+
+ //---------------------------
+ // defined in damageDef.txt. This will go away ( you can use damagedef_nuclear_core instead of eDamageSourceId.[enum id] and get rid of it from here )
+ // once this list has only damagedef_*, then we can remove eDamageSourceId
+ code_reserved // may be merged with invalid -1 above
+ damagedef_unknown // must start at 1 and order must match what's in damageDefs.txt
+ damagedef_unknownBugIt
+ damagedef_suicide
+ damagedef_despawn
+ damagedef_titan_step
+ damagedef_crush
+ damagedef_nuclear_core
+ damagedef_titan_fall
+ damagedef_titan_hotdrop
+ damagedef_reaper_fall
+ damagedef_trip_wire
+ damagedef_reaper_groundslam
+ damagedef_reaper_nuke
+ damagedef_frag_drone_explode
+ damagedef_frag_drone_explode_FD
+ damagedef_frag_drone_throwable_PLAYER
+ damagedef_frag_drone_throwable_NPC
+ damagedef_stalker_powersupply_explosion_small
+ damagedef_stalker_powersupply_explosion_large
+ damagedef_stalker_powersupply_explosion_large_at
+ damagedef_shield_captain_arc_shield
+ damagedef_fd_explosive_barrel
+ damagedef_fd_tether_trap
+
+ //---------------------------
+
+ // Titan Weapons
+ mp_titanweapon_40mm
+ mp_titanweapon_arc_cannon
+ mp_titanweapon_arc_wave
+ mp_titanweapon_arc_ball
+ mp_titanweapon_arc_pylon
+ mp_titanweapon_emp_volley
+ mp_titanweapon_rocket_launcher
+ mp_titanweapon_rocketeer_missile
+ mp_titanweapon_rocketeer_rocketstream
+ mp_titanweapon_shoulder_rockets
+ mp_titanweapon_shoulder_grenade
+ mp_titanweapon_orbital_strike
+ mp_titanweapon_tether_shot
+ mp_titanweapon_homing_rockets
+ mp_titanweapon_dumbfire_rockets
+ mp_titanweapon_multi_cluster
+ mp_titanweapon_meteor
+ mp_titanweapon_meteor_thermite
+ mp_titanweapon_meteor_thermite_charged
+ mp_titanweapon_salvo_rockets
+ mp_titanweapon_tracker_rockets
+ mp_titanweapon_sniper
+ mp_titanweapon_triple_threat
+ mp_titanweapon_vortex_shield
+ mp_titanweapon_vortex_shield_ion
+ mp_titanweapon_xo16
+ mp_titanweapon_xo16_shorty
+ mp_titanweapon_xopistol
+ mp_titanweapon_at_mine
+ mp_titanweapon_leadwall
+ mp_titanweapon_jackhammer
+ mp_titanweapon_electric_fist
+ mp_titanweapon_cabertoss
+ mp_titanweapon_flame_wall
+ mp_titanweapon_flame_ring
+ mp_titanweapon_smash
+ mp_titanweapon_particle_accelerator
+ mp_titanweapon_sticky_40mm
+ mp_titanweapon_predator_cannon
+ mp_titanweapon_predator_cannon_siege
+ mp_titanability_laser_trip
+ mp_titanweapon_laser_lite
+ mp_titanweapon_stun_laser
+ mp_titanability_smoke
+ mp_titanability_arc_field
+ mp_titanweapon_arc_minefield
+ mp_titanability_hover
+ mp_titanability_cloak
+ mp_titanability_tether_trap
+
+ mp_titancore_amp_core
+ mp_titancore_emp
+ mp_titancore_flame_wave
+ mp_titancore_flame_wave_secondary
+ mp_titancore_laser_cannon
+ mp_titancore_nuke_core
+ mp_titancore_nuke_missile
+ mp_titanweapon_berserker
+ mp_titancore_shift_core
+ mp_titanweapon_flightcore_rockets
+ mp_titancore_salvo_core
+ mp_titancore_siege_mode
+
+ //SP weapons
+ mp_weapon_grenade_electric_smoke
+ proto_titanweapon_deathblossom
+
+ // Pilot Weapons
+ mp_weapon_hemlok
+ mp_weapon_lmg
+ mp_weapon_rspn101
+ mp_weapon_vinson
+ mp_weapon_lstar
+ mp_weapon_g2
+ mp_weapon_smart_pistol
+ mp_weapon_r97
+ mp_weapon_car
+ mp_weapon_hemlok_smg
+ mp_weapon_dmr
+ mp_weapon_wingman
+ mp_weapon_wingman_n
+ mp_weapon_semipistol
+ mp_weapon_autopistol
+ mp_weapon_mgl
+ mp_weapon_sniper
+ mp_weapon_shotgun
+ mp_weapon_mastiff
+ mp_weapon_frag_drone
+ mp_weapon_frag_grenade
+ mp_weapon_grenade_emp
+ mp_weapon_arc_blast
+ mp_weapon_thermite_grenade
+ mp_weapon_grenade_sonar
+ mp_weapon_grenade_gravity
+ mp_weapon_satchel
+ mp_weapon_nuke_satchel
+ mp_weapon_proximity_mine
+ mp_weapon_smr
+ mp_weapon_rocket_launcher
+ mp_weapon_arc_launcher
+ mp_weapon_defender
+ mp_weapon_dash_melee
+ mp_weapon_tether
+ mp_weapon_tripwire
+ mp_weapon_flak_rifle
+ mp_extreme_environment
+ mp_weapon_shotgun_pistol
+ mp_weapon_pulse_lmg
+ mp_weapon_sword
+ mp_weapon_softball
+ mp_weapon_shotgun_doublebarrel
+ mp_weapon_doubletake
+ mp_weapon_arc_rifle
+ mp_weapon_gibber_pistol
+ mp_weapon_alternator_smg
+ mp_weapon_esaw
+ mp_weapon_epg
+ mp_weapon_arena1
+ mp_weapon_arena2
+ mp_weapon_arena3
+ mp_weapon_rspn101_og
+
+ //
+ melee_pilot_emptyhanded
+ melee_pilot_arena
+ melee_pilot_sword
+ melee_titan_punch
+ melee_titan_punch_ion
+ melee_titan_punch_tone
+ melee_titan_punch_legion
+ melee_titan_punch_scorch
+ melee_titan_punch_northstar
+ melee_titan_punch_fighter
+ melee_titan_punch_vanguard
+ melee_titan_sword
+ melee_titan_sword_aoe
+
+ mp_weapon_engineer_turret
+
+ // Turret Weapons
+ mp_weapon_yh803
+ mp_weapon_yh803_bullet
+ mp_weapon_yh803_bullet_overcharged
+ mp_weapon_mega_turret
+ mp_weapon_mega_turret_aa
+ mp_turretweapon_rockets
+ mp_turretweapon_blaster
+ mp_turretweapon_plasma
+ mp_turretweapon_sentry
+
+ // AI only Weapons
+ mp_weapon_super_spectre
+ mp_weapon_dronebeam
+ mp_weapon_dronerocket
+ mp_weapon_droneplasma
+ mp_weapon_turretplasma
+ mp_weapon_turretrockets
+ mp_weapon_turretplasma_mega
+ mp_weapon_gunship_launcher
+ mp_weapon_gunship_turret
+ mp_weapon_gunship_missile
+
+ // Misc
+ rodeo
+ rodeo_forced_titan_eject //For awarding points when you force a pilot to eject via rodeo
+ rodeo_execution
+ human_melee
+ auto_titan_melee
+ berserker_melee
+ mind_crime
+ charge_ball
+ grunt_melee
+ spectre_melee
+ prowler_melee
+ super_spectre_melee
+ titan_execution
+ human_execution
+ eviscerate
+ wall_smash
+ ai_turret
+ team_switch
+ rocket
+ titan_explosion
+ flash_surge
+ molotov
+ sticky_time_bomb
+ vortex_grenade
+ droppod_impact
+ ai_turret_explosion
+ rodeo_trap
+ round_end
+ bubble_shield
+ evac_dropship_explosion
+ sticky_explosive
+ titan_grapple
+
+ // streaks
+ satellite_strike
+
+ // Environmental
+ fall
+ splat
+ crushed
+ burn
+ lasergrid
+ outOfBounds
+ indoor_inferno
+ submerged
+ switchback_trap
+ floor_is_lava
+ suicideSpectreAoE
+ titanEmpField
+ stuck
+ deadly_fog
+ exploding_barrel
+ electric_conduit
+ turbine
+ harvester_beam
+ toxic_sludge
+
+ mp_weapon_spectre_spawner
+
+ // development
+ weapon_cubemap
+
+ // Prototype
+ mp_weapon_zipline
+ mp_ability_ground_slam
+ sp_weapon_arc_tool
+ sp_weapon_proto_battery_charger_offhand
+ at_turret_override
+ rodeo_battery_removal
+ phase_shift
+ gamemode_bomb_detonation
+ nuclear_turret
+ proto_viewmodel_test
+ mp_titanweapon_heat_shield
+ mp_titanability_slow_trap
+ mp_titanability_gun_shield
+ mp_titanability_power_shot
+ mp_titanability_ammo_swap
+ mp_titanability_sonar_pulse
+ mp_titanability_rearm
+ mp_titancore_upgrade
+ mp_titanweapon_xo16_vanguard
+ mp_weapon_arc_trap
+ core_overload
+
+ bombardment
+ bleedout
+ //damageSourceId=eDamageSourceId.xxxxx
+ //fireteam
+ //marvin
+ //rocketstrike
+ //orbitallaser
+ //explosion
+}
+
+//When adding new mods, they need to be added below and to persistent_player_data_version_N.pdef in r1/cfg/server.
+//Then when updating that file, save a new one and increment N.
+
+global enum eModSourceId
+{
+ accelerator
+ afterburners
+ arc_triple_threat
+ aog
+ burn_mod_autopistol
+ burn_mod_car
+ burn_mod_defender
+ burn_mod_dmr
+ burn_mod_emp_grenade
+ burn_mod_frag_grenade
+ burn_mod_grenade_electric_smoke
+ burn_mod_grenade_gravity
+ burn_mod_thermite_grenade
+ burn_mod_g2
+ burn_mod_hemlok
+ burn_mod_lmg
+ burn_mod_mgl
+ burn_mod_r97
+ burn_mod_rspn101
+ burn_mod_satchel
+ burn_mod_semipistol
+ burn_mod_smart_pistol
+ burn_mod_smr
+ burn_mod_sniper
+ burn_mod_rocket_launcher
+ burn_mod_titan_40mm
+ burn_mod_titan_arc_cannon
+ burn_mod_titan_rocket_launcher
+ burn_mod_titan_sniper
+ burn_mod_titan_triple_threat
+ burn_mod_titan_xo16
+ burn_mod_titan_dumbfire_rockets
+ burn_mod_titan_homing_rockets
+ burn_mod_titan_salvo_rockets
+ burn_mod_titan_shoulder_rockets
+ burn_mod_titan_vortex_shield
+ burn_mod_titan_smoke
+ burn_mod_titan_particle_wall
+ burst
+ capacitor
+ enhanced_targeting
+ extended_ammo
+ fast_lock
+ fast_reload
+ guided_missile
+ hcog
+ holosight
+ instant_shot
+ iron_sights
+ overcharge
+ quick_shot
+ rapid_fire_missiles
+ scope_4x
+ scope_6x
+ scope_8x
+ scope_10x
+ scope_12x
+ burn_mod_shotgun
+ silencer
+ slammer
+ spread_increase_ttt
+ stabilizer
+ titanhammer
+ burn_mod_wingman
+ burn_mod_lstar
+ burn_mod_mastiff
+ burn_mod_vinson
+ ricochet
+ ar_trajectory
+ redline_sight
+ threat_scope
+ smart_lock
+ pro_screen
+ rocket_arena
+}
+
+//Attachments intentionally left off. This prevents them from displaying in kill cards.
+// modNameStrings should be defined when the mods are created, not in a separate table -Mackey
+global const modNameStrings = {
+ [ eModSourceId.accelerator ] = "#MOD_ACCELERATOR_NAME",
+ [ eModSourceId.afterburners ] = "#MOD_AFTERBURNERS_NAME",
+ [ eModSourceId.arc_triple_threat ] = "#MOD_ARC_TRIPLE_THREAT_NAME",
+ [ eModSourceId.burn_mod_autopistol ] = "#BC_AUTOPISTOL_M2",
+ [ eModSourceId.burn_mod_car ] = "#BC_CAR_M2",
+ [ eModSourceId.burn_mod_defender ] = "#BC_DEFENDER_M2",
+ [ eModSourceId.burn_mod_dmr ] = "#BC_DMR_M2",
+ [ eModSourceId.burn_mod_emp_grenade ] = "#BC_EMP_GRENADE_M2",
+ [ eModSourceId.burn_mod_frag_grenade ] = "#BC_FRAG_GRENADE_M2",
+ [ eModSourceId.burn_mod_grenade_electric_smoke ] = "#BC_GRENADE_ELECTRIC_SMOKE_M2",
+ [ eModSourceId.burn_mod_grenade_gravity ] = "#BC_GRENADE_ELECTRIC_SMOKE_M2",
+ [ eModSourceId.burn_mod_thermite_grenade ] = "#BC_GRENADE_ELECTRIC_SMOKE_M2",
+ [ eModSourceId.burn_mod_g2 ] = "#BC_G2_M2",
+ [ eModSourceId.burn_mod_hemlok ] = "#BC_HEMLOK_M2",
+ [ eModSourceId.burn_mod_lmg ] = "#BC_LMG_M2",
+ [ eModSourceId.burn_mod_mgl ] = "#BC_MGL_M2",
+ [ eModSourceId.burn_mod_r97 ] = "#BC_R97_M2",
+ [ eModSourceId.burn_mod_rspn101 ] = "#BC_RSPN101_M2",
+ [ eModSourceId.burn_mod_satchel ] = "#BC_SATCHEL_M2",
+ [ eModSourceId.burn_mod_semipistol ] = "#BC_SEMIPISTOL_M2",
+ [ eModSourceId.burn_mod_smr ] = "#BC_SMR_M2",
+ [ eModSourceId.burn_mod_smart_pistol ] = "#BC_SMART_PISTOL_M2",
+ [ eModSourceId.burn_mod_sniper ] = "#BC_SNIPER_M2",
+ [ eModSourceId.burn_mod_rocket_launcher ] = "#BC_ROCKET_LAUNCHER_M2",
+ [ eModSourceId.burn_mod_titan_40mm ] = "#BC_TITAN_40MM_M2",
+ [ eModSourceId.burn_mod_titan_arc_cannon ] = "#BC_TITAN_ARC_CANNON_M2",
+ [ eModSourceId.burn_mod_titan_rocket_launcher ] = "#BC_TITAN_ROCKET_LAUNCHER_M2",
+ [ eModSourceId.burn_mod_titan_sniper ] = "#BC_TITAN_SNIPER_M2",
+ [ eModSourceId.burn_mod_titan_triple_threat ] = "#BC_TITAN_TRIPLE_THREAT_M2",
+ [ eModSourceId.burn_mod_titan_xo16 ] = "#BC_TITAN_XO16_M2",
+ [ eModSourceId.burn_mod_titan_dumbfire_rockets ] = "#BC_TITAN_DUMBFIRE_MISSILE_M2",
+ [ eModSourceId.burn_mod_titan_homing_rockets ] = "#BC_TITAN_HOMING_ROCKETS_M2",
+ [ eModSourceId.burn_mod_titan_salvo_rockets ] = "#BC_TITAN_SALVO_ROCKETS_M2",
+ [ eModSourceId.burn_mod_titan_shoulder_rockets ] = "#BC_TITAN_SHOULDER_ROCKETS_M2",
+ [ eModSourceId.burn_mod_titan_vortex_shield ] = "#BC_TITAN_VORTEX_SHIELD_M2",
+ [ eModSourceId.burn_mod_titan_smoke ] = "#BC_TITAN_ELECTRIC_SMOKE_M2",
+ [ eModSourceId.burn_mod_titan_particle_wall ] = "#BC_TITAN_SHIELD_WALL_M2",
+ [ eModSourceId.burst ] = "#MOD_BURST_NAME",
+ [ eModSourceId.capacitor ] = "#MOD_CAPACITOR_NAME",
+ [ eModSourceId.enhanced_targeting ] = "#MOD_ENHANCED_TARGETING_NAME",
+ [ eModSourceId.extended_ammo ] = "#MOD_EXTENDED_MAG_NAME",
+ [ eModSourceId.fast_reload ] = "#MOD_FAST_RELOAD_NAME",
+ [ eModSourceId.instant_shot ] = "#MOD_INSTANT_SHOT_NAME",
+ [ eModSourceId.overcharge ] = "#MOD_OVERCHARGE_NAME",
+ [ eModSourceId.quick_shot ] = "#MOD_QUICK_SHOT_NAME",
+ [ eModSourceId.rapid_fire_missiles ] = "#MOD_RAPID_FIRE_MISSILES_NAME",
+ [ eModSourceId.burn_mod_shotgun ] = "#BC_SHOTGUN_M2",
+ [ eModSourceId.silencer ] = "#MOD_SILENCER_NAME",
+ [ eModSourceId.slammer ] = "#MOD_SLAMMER_NAME",
+ [ eModSourceId.spread_increase_ttt ] = "#MOD_SPREAD_INCREASE_TTT_NAME",
+ [ eModSourceId.stabilizer ] = "#MOD_STABILIZER_NAME",
+ [ eModSourceId.titanhammer ] = "#MOD_TITANHAMMER_NAME",
+ [ eModSourceId.burn_mod_wingman ] = "#BC_WINGMAN_M2",
+ [ eModSourceId.burn_mod_lstar ] = "#BC_LSTAR_M2",
+ [ eModSourceId.burn_mod_mastiff ] = "#BC_MASTIFF_M2",
+ [ eModSourceId.burn_mod_vinson ] = "#BC_VINSON_M2",
+ [ eModSourceId.ricochet ] = "Ricochet",
+ [ eModSourceId.ar_trajectory ] = "AR Trajectory",
+ [ eModSourceId.smart_lock ] = "Smart Lock",
+ [ eModSourceId.pro_screen ] = "Pro Screen",
+ [ eModSourceId.rocket_arena ] = "Rocket Arena",
+}
+
+void function DamageTypes_Init()
+{
+ #if SERVER
+ AddCallback_OnClientConnected( SendNewDamageSourceIDsConnected )
+ #else
+ AddServerToClientStringCommandCallback( "register_damage_source_ids", ReceiveNewDamageSourceIDs )
+ #endif
+
+ foreach ( name, number in eDamageSourceId )
+ {
+ file.damageSourceIDToString[ number ] <- name
+ }
+
+ PrecacheWeapon( "mp_weapon_rspn101" ) // used by npc_soldier ><
+
+#if DEV
+
+ int numDamageDefs = DamageDef_GetCount()
+ table damageSourceIdEnum = expect table( getconsttable().eDamageSourceId )
+ foreach ( name, id in damageSourceIdEnum )
+ {
+ expect int( id )
+ if ( id <= eDamageSourceId.code_reserved || id >= numDamageDefs )
+ continue
+
+ string damageDefName = DamageDef_GetName( id )
+ Assert( damageDefName == name, "damage def (" + id + ") name: '" + damageDefName + "' doesn't match damage source id '" + name + "'" )
+ }
+#endif
+
+ file.damageSourceIDToName =
+ {
+ //sp
+ [ eDamageSourceId.mp_weapon_grenade_electric_smoke ] = "#DEATH_ELECTRIC_SMOKE_SCREEN",
+ [ eDamageSourceId.proto_titanweapon_deathblossom ] = "#WPN_TITAN_ROCKET_LAUNCHER",
+
+ //mp
+ [ eDamageSourceId.mp_extreme_environment ] = "#DAMAGE_EXTREME_ENVIRONMENT",
+
+ [ eDamageSourceId.mp_weapon_engineer_turret ] = "#WPN_ENGINEER_TURRET",
+
+ [ eDamageSourceId.mp_weapon_yh803 ] = "#WPN_LIGHT_TURRET",
+ [ eDamageSourceId.mp_weapon_yh803_bullet ] = "#WPN_LIGHT_TURRET",
+ [ eDamageSourceId.mp_weapon_yh803_bullet_overcharged ] = "#WPN_LIGHT_TURRET",
+ [ eDamageSourceId.mp_weapon_mega_turret ] = "#WPN_MEGA_TURRET",
+ [ eDamageSourceId.mp_weapon_mega_turret_aa ] = "#WPN_MEGA_TURRET",
+ [ eDamageSourceId.mp_turretweapon_rockets ] = "#WPN_TURRET_ROCKETS",
+ [ eDamageSourceId.mp_weapon_super_spectre ] = "#WPN_SUPERSPECTRE_ROCKETS",
+ [ eDamageSourceId.mp_weapon_dronebeam ] = "#WPN_DRONERBEAM",
+ [ eDamageSourceId.mp_weapon_dronerocket ] = "#WPN_DRONEROCKET",
+ [ eDamageSourceId.mp_weapon_droneplasma ] = "#WPN_DRONEPLASMA",
+ [ eDamageSourceId.mp_weapon_turretplasma ] = "#WPN_TURRETPLASMA",
+ [ eDamageSourceId.mp_weapon_turretrockets ] = "#WPN_TURRETROCKETS",
+ [ eDamageSourceId.mp_weapon_turretplasma_mega ] = "#WPN_TURRETPLASMA_MEGA",
+ [ eDamageSourceId.mp_weapon_gunship_launcher ] = "#WPN_GUNSHIP_LAUNCHER",
+ [ eDamageSourceId.mp_weapon_gunship_turret ] = "#WPN_GUNSHIP_TURRET",
+ [ eDamageSourceId.mp_weapon_gunship_turret ] = "#WPN_GUNSHIP_MISSILE",
+
+ [ eDamageSourceId.mp_titanability_smoke ] = "#DEATH_ELECTRIC_SMOKE_SCREEN",
+ [ eDamageSourceId.mp_titanability_laser_trip ] = "#DEATH_LASER_TRIPWIRE",
+ [ eDamageSourceId.mp_titanability_slow_trap ] = "#DEATH_SLOW_TRAP",
+ [ eDamageSourceId.mp_titanability_tether_trap ] = "#DEATH_TETHER_TRAP",
+
+ [ eDamageSourceId.rodeo ] = "#DEATH_TITAN_RODEO",
+ [ eDamageSourceId.rodeo_forced_titan_eject ] = "#DEATH_TITAN_RODEO",
+ [ eDamageSourceId.rodeo_execution ] = "#DEATH_RODEO_EXECUTION",
+ [ eDamageSourceId.nuclear_turret ] = "#DEATH_NUCLEAR_TURRET",
+ [ eDamageSourceId.mp_titanweapon_flightcore_rockets ] = "#WPN_TITAN_FLIGHT_ROCKET",
+ [ eDamageSourceId.mp_titancore_amp_core ] = "#TITANCORE_AMP_CORE",
+ [ eDamageSourceId.mp_titancore_emp ] = "#TITANCORE_EMP",
+ [ eDamageSourceId.mp_titancore_siege_mode ] = "#TITANCORE_SIEGE_MODE",
+ [ eDamageSourceId.mp_titancore_flame_wave ] = "#TITANCORE_FLAME_WAVE",
+ [ eDamageSourceId.mp_titancore_flame_wave_secondary ] = "#GEAR_SCORCH_FLAMECORE",
+ [ eDamageSourceId.mp_titancore_nuke_core ] = "#TITANCORE_NUKE",
+ [ eDamageSourceId.mp_titancore_nuke_missile ] = "#TITANCORE_NUKE_MISSILE",
+ [ eDamageSourceId.mp_titancore_shift_core ] = "#TITANCORE_SWORD",
+ [ eDamageSourceId.berserker_melee ] = "#DEATH_BERSERKER_MELEE",
+ [ eDamageSourceId.human_melee ] = "#DEATH_HUMAN_MELEE",
+ [ eDamageSourceId.auto_titan_melee ] = "#DEATH_AUTO_TITAN_MELEE",
+
+ [ eDamageSourceId.prowler_melee ] = "#DEATH_PROWLER_MELEE",
+ [ eDamageSourceId.super_spectre_melee ] = "#DEATH_SUPER_SPECTRE",
+ [ eDamageSourceId.grunt_melee ] = "#DEATH_GRUNT_MELEE",
+ [ eDamageSourceId.spectre_melee ] = "#DEATH_SPECTRE_MELEE",
+ [ eDamageSourceId.eviscerate ] = "#DEATH_EVISCERATE",
+ [ eDamageSourceId.wall_smash ] = "#DEATH_WALL_SMASH",
+ [ eDamageSourceId.ai_turret ] = "#DEATH_TURRET",
+ [ eDamageSourceId.team_switch ] = "#DEATH_TEAM_CHANGE",
+ [ eDamageSourceId.rocket ] = "#DEATH_ROCKET",
+ [ eDamageSourceId.titan_explosion ] = "#DEATH_TITAN_EXPLOSION",
+ [ eDamageSourceId.evac_dropship_explosion ] = "#DEATH_EVAC_DROPSHIP_EXPLOSION",
+ [ eDamageSourceId.flash_surge ] = "#DEATH_FLASH_SURGE",
+ [ eDamageSourceId.molotov ] = "#DEATH_MOLOTOV",
+ [ eDamageSourceId.sticky_time_bomb ] = "#DEATH_STICKY_TIME_BOMB",
+ [ eDamageSourceId.vortex_grenade ] = "#DEATH_VORTEX_GRENADE",
+ [ eDamageSourceId.droppod_impact ] = "#DEATH_DROPPOD_CRUSH",
+ [ eDamageSourceId.ai_turret_explosion ] = "#DEATH_TURRET_EXPLOSION",
+ [ eDamageSourceId.rodeo_trap ] = "#DEATH_RODEO_TRAP",
+ [ eDamageSourceId.round_end ] = "#DEATH_ROUND_END",
+ [ eDamageSourceId.burn ] = "#DEATH_BURN",
+ [ eDamageSourceId.mind_crime ] = "Mind Crime",
+ [ eDamageSourceId.charge_ball ] = "Charge Ball",
+ [ eDamageSourceId.mp_titanweapon_rocketeer_missile ] = "Rocketeer Missile",
+ [ eDamageSourceId.core_overload ] = "#DEATH_CORE_OVERLOAD",
+ [ eDamageSourceId.mp_weapon_arc_trap ] = "#WPN_ARC_TRAP",
+
+
+ [ eDamageSourceId.mp_turretweapon_sentry ] = "#WPN_SENTRY_TURRET",
+ [ eDamageSourceId.mp_turretweapon_blaster ] = "#WPN_BLASTER_TURRET",
+ [ eDamageSourceId.mp_turretweapon_rockets ] = "#WPN_ROCKET_TURRET",
+ [ eDamageSourceId.mp_turretweapon_plasma ] = "#WPN_PLASMA_TURRET",
+
+ [ eDamageSourceId.bubble_shield ] = "#DEATH_BUBBLE_SHIELD",
+ [ eDamageSourceId.sticky_explosive ] = "#DEATH_STICKY_EXPLOSIVE",
+ [ eDamageSourceId.titan_grapple ] = "#DEATH_TITAN_GRAPPLE",
+
+ [ eDamageSourceId.satellite_strike ] = "#DEATH_SATELLITE_STRIKE",
+
+ [ eDamageSourceId.mp_titanweapon_meteor ] = "#WPN_TITAN_METEOR",
+ [ eDamageSourceId.mp_titanweapon_meteor_thermite ] = "#WPN_TITAN_METEOR",
+ [ eDamageSourceId.mp_titanweapon_meteor_thermite_charged ] = "Thermite Meteor",
+ [ eDamageSourceId.mp_titanweapon_flame_ring ] = "Flame Wreath",
+
+ // Instant death. Show no percentages on death recap.
+ [ eDamageSourceId.fall ] = "#DEATH_FALL",
+ //Todo: Rename eDamageSourceId.splat with a more appropriate name. This damage type was used for enviornmental damage, but it was for eject killing pilots if they were near a ceiling. I've changed the localized string to "Enviornment Damage", but this will cause confusion in the future.
+ [ eDamageSourceId.splat ] = "#DEATH_SPLAT",
+ [ eDamageSourceId.titan_execution ] = "#DEATH_TITAN_EXECUTION",
+ [ eDamageSourceId.human_execution ] = "#DEATH_HUMAN_EXECUTION",
+ [ eDamageSourceId.outOfBounds ] = "#DEATH_OUT_OF_BOUNDS",
+ [ eDamageSourceId.indoor_inferno ] = "#DEATH_INDOOR_INFERNO",
+ [ eDamageSourceId.submerged ] = "#DEATH_SUBMERGED",
+ [ eDamageSourceId.switchback_trap ] = "#DEATH_ELECTROCUTION", // Damages teammates and opposing team
+ [ eDamageSourceId.floor_is_lava ] = "#DEATH_ELECTROCUTION",
+ [ eDamageSourceId.suicideSpectreAoE ] = "#DEATH_SUICIDE_SPECTRE", // Used for distinguishing the initial spectre from allies.
+ [ eDamageSourceId.titanEmpField ] = "#DEATH_TITAN_EMP_FIELD",
+ [ eDamageSourceId.deadly_fog ] = "#DEATH_DEADLY_FOG",
+
+
+ // Prototype
+ [ eDamageSourceId.mp_weapon_zipline ] = "Zipline",
+ [ eDamageSourceId.mp_ability_ground_slam ] = "Ground Slam",
+ [ eDamageSourceId.sp_weapon_arc_tool ] = "#WPN_ARC_TOOL",
+ [ eDamageSourceId.sp_weapon_proto_battery_charger_offhand ] = "Battery Charger",
+ [ eDamageSourceId.at_turret_override ] = "AT Turret",
+ [ eDamageSourceId.phase_shift ] = "#WPN_SHIFTER",
+ [ eDamageSourceId.gamemode_bomb_detonation ] = "Bomb Detonation",
+ [ eDamageSourceId.bleedout ] = "#DEATH_BLEEDOUT",
+
+ [ eDamageSourceId.damagedef_unknownBugIt ] = "#DEATH_GENERIC_KILLED",
+ [ eDamageSourceId.damagedef_unknown ] = "#DEATH_GENERIC_KILLED",
+ [ eDamageSourceId.weapon_cubemap ] = "#DEATH_GENERIC_KILLED",
+ [ eDamageSourceId.stuck ] = "#DEATH_GENERIC_KILLED",
+ [ eDamageSourceId.rodeo_battery_removal ] = "#DEATH_RODEO_BATTERY_REMOVAL",
+
+ [ eDamageSourceId.melee_pilot_emptyhanded ] = "#DEATH_MELEE",
+ [ eDamageSourceId.melee_pilot_arena ] = "#DEATH_MELEE",
+ [ eDamageSourceId.melee_pilot_sword ] = "#DEATH_SWORD",
+ [ eDamageSourceId.melee_titan_punch ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_ion ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_tone ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_northstar ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_scorch ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_legion ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_fighter ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_punch_vanguard ] = "#DEATH_TITAN_MELEE",
+ [ eDamageSourceId.melee_titan_sword ] = "#DEATH_TITAN_SWORD",
+ [ eDamageSourceId.melee_titan_sword_aoe ] = "#DEATH_TITAN_SWORD",
+ [ eDamageSourceId.mp_titanweapon_arc_cannon ] = "#WPN_TITAN_ARC_CANNON_SHORT",
+ [ eDamageSourceId.mp_weapon_shotgun_doublebarrel ] = "#WPN_SHOTGUN_DBLBARREL_SHORT"
+ }
+
+ #if DEV
+ //development, with retail versions incase a rare bug happens we dont want to show developer text
+ file.damageSourceIDToName[ eDamageSourceId.damagedef_unknownBugIt ] = "UNKNOWN! BUG IT!"
+ file.damageSourceIDToName[ eDamageSourceId.damagedef_unknown ] = "Unknown"
+ file.damageSourceIDToName[ eDamageSourceId.weapon_cubemap ] = "Cubemap"
+ //file.damageSourceIDToName[ eDamageSourceId.invalid ] = "INVALID (BUG IT!)"
+ file.damageSourceIDToName[ eDamageSourceId.stuck ] = "NPC got Stuck (Don't Bug it!)"
+ #endif
+}
+
+void function RegisterWeaponDamageSourceName( string weaponRef, string damageSourceName )
+{
+ int sourceID = eDamageSourceId[weaponRef]
+ file.damageSourceIDToName[ sourceID ] <- damageSourceName
+}
+
+bool function DamageSourceIDHasString( int index )
+{
+ return (index in file.damageSourceIDToString)
+}
+
+string function DamageSourceIDToString( int index )
+{
+ return file.damageSourceIDToString[ index ]
+}
+
+string function GetObitFromDamageSourceID( int damageSourceID )
+{
+ if ( damageSourceID > 0 && damageSourceID < DamageDef_GetCount() )
+ {
+ return DamageDef_GetObituary( damageSourceID )
+ }
+
+ if ( damageSourceID in file.damageSourceIDToName )
+ return file.damageSourceIDToName[ damageSourceID ]
+
+ table damageSourceIdEnum = expect table( getconsttable().eDamageSourceId )
+ foreach ( name, id in damageSourceIdEnum )
+ {
+ if ( id == damageSourceID )
+ return expect string( name )
+ }
+
+ return ""
+}
+
+#if SERVER
+void function RegisterWeaponDamageSource( string weaponRef, string damageSourceName )
+{
+ // Have to do this since squirrel table initialization only supports literals for string keys
+ table< string, string > temp
+ temp[ weaponRef ] <- damageSourceName
+ RegisterWeaponDamageSources( temp )
+}
+
+/* Values are expected to be in a table containing the enum variable name and the string name, e.g.
+ {"mp_titanweapon_sniper" : "Plasma Railgun", "mp_titanweapon_meteor" : "T203 Thermite Launcher"}
+ Only works properly if used after the match starts, e.g. called in "after" callbacks.
+*/
+void function RegisterWeaponDamageSources( table< string, string > newValueTable )
+{
+ int trgt = file.damageSourceIDToString.len() - 1 // -1 accounts for invalid.
+ int lastCustomSize = file.customDamageSourceIDList.len() // Used to only send new IDs to clients if any are added during runtime.
+
+ foreach ( newVal, stringVal in newValueTable )
+ {
+ // Don't replace existing enum values
+ while ( trgt in file.damageSourceIDToString )
+ trgt++
+
+ // Only move insertion point if insertion succeeded
+ if ( RegisterWeaponDamageSourceInternal( trgt, newVal, stringVal ) )
+ trgt++;
+ }
+
+ // Send IDs created during match runtime. IDs made on inits get sent through client connected callback.
+ foreach( player in GetPlayerArray() )
+ SendNewDamageSourceIDs( player, lastCustomSize )
+}
+#endif
+
+bool function RegisterWeaponDamageSourceInternal( int id, string newVal, string stringVal )
+{
+ table damageSourceID = expect table( getconsttable()[ "eDamageSourceId" ] )
+
+ // Fail invalid new source IDs (already exists or cannot be sent via string commands). Length condition has loose padding to account for ID string length.
+ if ( newVal in damageSourceID || newVal.len() + stringVal.len() > SOURCE_ID_MAX_MESSAGE_LENGTH - 15 || id in file.damageSourceIDToString )
+ return false
+
+ damageSourceID[ newVal ] <- id
+ file.damageSourceIDToString[ id ] <- newVal
+ file.damageSourceIDToName[ id ] <- stringVal
+ file.customDamageSourceIDList.extend( [ id.tostring(), newVal, StringReplace( stringVal, " ", MESSAGE_SPACE_PADDING, true ) ] )
+ return true
+}
+
+#if SERVER
+void function SendNewDamageSourceIDsConnected( entity player )
+{
+ SendNewDamageSourceIDs( player )
+}
+
+void function SendNewDamageSourceIDs( entity player, int index = 0 )
+{
+ while ( index < file.customDamageSourceIDList.len() )
+ {
+ int curSize = 0
+ int curIndex = index
+
+ // Figure out how many sources to send in this message chunk
+ while ( curIndex < file.customDamageSourceIDList.len() )
+ {
+ // Sources are inserted to the custom list in triplets, so we can trust these indices exist.
+ curSize += file.customDamageSourceIDList[ curIndex ].len()
+ curSize += file.customDamageSourceIDList[ curIndex + 1 ].len()
+ curSize += file.customDamageSourceIDList[ curIndex + 2 ].len()
+
+ // Stop before including strings in current message if it exceeds max message length.
+ // This will never stall on a singular source that exceeds the size since new sources are size limited.
+ if ( curSize > SOURCE_ID_MAX_MESSAGE_LENGTH )
+ break
+
+ curIndex += 3
+ }
+
+ // Create the string to pass to client
+ string message = ""
+ while ( index < curIndex )
+ message += file.customDamageSourceIDList[ index++ ] + " "
+
+ ServerToClientStringCommand( player, "register_damage_source_ids " + message )
+ }
+}
+#else
+void function ReceiveNewDamageSourceIDs( array<string> args )
+{
+ // IDs are inserted to the custom list in triplets, so we can trust these indices exist and the loop will end properly
+ for ( int i = 0; i < args.len(); i += 3 )
+ RegisterWeaponDamageSourceInternal( args[ i ].tointeger(), args[ i + 1 ], StringReplace( args[ i + 2 ], MESSAGE_SPACE_PADDING, " ", true ) )
+}
+#endif
diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut
new file mode 100644
index 00000000..4cfdc6fb
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/sh_message_utils.gnut
@@ -0,0 +1,505 @@
+#if SERVER
+global function MessageUtils_ServerInit
+
+global function NSCreatePollOnPlayer
+global function NSGetPlayerResponse
+
+global function NSSendLargeMessageToPlayer
+global function NSSendPopUpMessageToPlayer
+global function NSSendAnnouncementMessageToPlayer
+global function NSSendInfoMessageToPlayer
+
+global function NSCreateStatusMessageOnPlayer
+global function NSEditStatusMessageOnPlayer
+global function NSDeleteStatusMessageOnPlayer
+
+struct
+{
+ table<entity,int> playerPollResponses
+} server
+#endif // SERVER
+
+
+#if CLIENT
+global function MessageUtils_ClientInit
+
+vector ColorSelected = < 0.9, 0.8, 0.5 >
+vector ColorBase = < 0.9, 0.5, 0.1 >
+
+struct tempMessage
+{
+ string title
+ string description
+ float duration
+ string image
+ int priority
+ int style
+ vector color
+}
+
+
+// Nested structs look funny, but are pretty helpful when reading code so I'm keeping them :)
+struct
+{
+ struct
+ {
+ string header
+ array<string> options
+ float duration
+ bool pollActive
+ array<var> ruis
+ } poll
+
+ string id
+ tempMessage temp
+
+ array<tempMessage> largeMessageQueue
+ array<tempMessage> popupMessageQueue
+ array<tempMessage> announcementQueue
+ array<tempMessage> infoMessageQueue
+
+ // table<id,rui>
+ table<string,var> statusMessageList
+} client
+#endif // CLIENT
+
+
+const int STATUS_MESSAGES_MAX = 4
+
+
+enum eMessageType
+{
+ POLL,
+ LARGE,
+ POPUP,
+ ANNOUNCEMENT,
+ INFO,
+ CREATE_STATUS,
+ EDIT_STATUS,
+ DELETE_STATUS
+}
+
+enum eDataType
+{
+ POLL_HEADER,
+ POLL_OPTION,
+ POLL_DURATION,
+ POLL_SELECT,
+ TITLE,
+ DESC,
+ DURATION,
+ ASSET,
+ COLOR,
+ PRIORITY,
+ STYLE,
+ ID
+}
+
+#if SERVER
+void function MessageUtils_ServerInit()
+{
+ AddClientCommandCallback( "vote", ClientCommand_Vote )
+ AddClientCommandCallback( "poll_respond", ClientCommand_PollRespond )
+}
+
+bool function ClientCommand_Vote( entity player, array<string> args )
+{
+ if( args.len() == 0 )
+ return false
+
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.POLL_SELECT + " " + args[0] )
+ return true
+}
+
+bool function ClientCommand_PollRespond( entity player, array<string> args )
+{
+ if( args.len() == 0 )
+ return false
+
+ server.playerPollResponses[player] <- args[0].tointeger()
+ return true
+}
+
+void function NSCreateStatusMessageOnPlayer( entity player, string title, string description, string id )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.TITLE + " " + title )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + description )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.ID + " " + id )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.CREATE_STATUS )
+}
+
+void function NSEditStatusMessageOnPlayer( entity player, string title, string description, string id )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.TITLE + " " + title )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + description )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.ID + " " + id )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.EDIT_STATUS )
+}
+
+void function NSDeleteStatusMessageOnPlayer( entity player, string id )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.ID + " " + id )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.DELETE_STATUS )
+}
+
+void function NSCreatePollOnPlayer( entity player, string header, array<string> options, float duration )
+{
+ foreach ( string option in options )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.POLL_OPTION + " " + option )
+
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.POLL_DURATION + " " + duration )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.POLL_HEADER + " " + header )
+
+ server.playerPollResponses[player] <- -1 // Reset poll response table
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.POLL )
+}
+
+int function NSGetPlayerResponse( entity player )
+{
+ if( !( player in server.playerPollResponses ) )
+ return -1
+
+ if( server.playerPollResponses[ player ] == -1 )
+ return -1
+
+ return server.playerPollResponses[ player ] - 1
+}
+
+void function NSSendLargeMessageToPlayer( entity player, string title, string description, float duration, string image )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.TITLE + " " + title )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + description )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DURATION + " " + duration )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.ASSET + " " + image )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.LARGE )
+}
+
+void function NSSendPopUpMessageToPlayer( entity player, string text )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + text )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.POPUP )
+}
+
+void function NSSendAnnouncementMessageToPlayer( entity player, string title, string description, vector color, int priority, int style )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.TITLE + " " + title )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + description )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.COLOR + " " + color.x + " " + color.y + " " + color.z )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.PRIORITY + " " + priority )
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.STYLE + " " + style )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.ANNOUNCEMENT )
+}
+
+void function NSSendInfoMessageToPlayer( entity player, string text )
+{
+ ServerToClientStringCommand( player, "ServerHUDMessagePut " + eDataType.DESC + " " + text )
+
+ ServerToClientStringCommand( player, "ServerHUDMessageShow " + eMessageType.INFO )
+}
+
+#endif // SERVER
+
+#if CLIENT
+void function MessageUtils_ClientInit()
+{
+ // ServerHUDMessageRequest <eMessageType>
+ AddServerToClientStringCommandCallback( "ServerHUDMessageShow", ServerCallback_CreateServerHUDMessage )
+ // ServerHUDMessageRequest <eDataType> <Data>
+ AddServerToClientStringCommandCallback( "ServerHUDMessagePut", ServerCallback_UpdateServerHUDMessage )
+
+ thread LargeMessageHandler_Threaded()
+ thread PopUpMessageHandler_Threaded()
+ thread AnnouncementMessageHandler_Threaded()
+ thread InfoMessageHandler_Threaded()
+}
+
+string function CombineArgsIntoString( array<string> args )
+{
+ string result
+
+ // Ignore the first argument
+ for( int i = 1; i < args.len(); i++ )
+ result += Localize( args[i] ) + " "
+
+ return result
+}
+
+void function ServerCallback_UpdateServerHUDMessage ( array<string> args )
+{
+ switch ( args[0].tointeger() )
+ {
+ case eDataType.POLL_HEADER:
+ client.poll.header = CombineArgsIntoString( args )
+ break
+ case eDataType.POLL_OPTION:
+ client.poll.options.append( CombineArgsIntoString( args ) )
+ break
+ case eDataType.POLL_DURATION:
+ client.poll.duration = args[1].tofloat()
+ break
+ case eDataType.POLL_SELECT:
+ thread SelectPollOption_Threaded( args[1].tointeger() )
+ break
+ case eDataType.TITLE:
+ client.temp.title = CombineArgsIntoString( args )
+ break
+ case eDataType.DESC:
+ client.temp.description = CombineArgsIntoString( args )
+ break
+ case eDataType.DURATION:
+ client.temp.duration = args[1].tofloat()
+ break
+ case eDataType.ASSET:
+ client.temp.image = CombineArgsIntoString( args )
+ break
+ case eDataType.COLOR:
+ client.temp.color = Vector( args[1].tofloat(), args[2].tofloat(), args[3].tofloat())
+ break
+ case eDataType.PRIORITY:
+ client.temp.priority = args[1].tointeger()
+ break
+ case eDataType.STYLE:
+ client.temp.style = args[1].tointeger()
+ break
+ case eDataType.ID:
+ client.id = args[1]
+ break
+ }
+}
+
+void function ServerCallback_CreateServerHUDMessage ( array<string> args )
+{
+ switch ( args[0].tointeger() )
+ {
+ case eMessageType.POLL:
+ thread ShowPollMessage_Threaded()
+ break
+ case eMessageType.LARGE:
+ client.largeMessageQueue.append( client.temp )
+ break
+ case eMessageType.POPUP:
+ client.popupMessageQueue.append( client.temp )
+ break
+ case eMessageType.ANNOUNCEMENT:
+ client.announcementQueue.append( client.temp )
+ break
+ case eMessageType.INFO:
+ client.infoMessageQueue.append( client.temp )
+ break
+ case eMessageType.CREATE_STATUS:
+ CreateStatusMessage( client.id )
+ break
+ case eMessageType.EDIT_STATUS:
+ EditStatusMessage( client.id )
+ break
+ case eMessageType.DELETE_STATUS:
+ thread DeleteStatusMessage( client.id )
+ break
+ }
+}
+
+void function DeleteStatusMessage( string id )
+{
+ if ( id in client.statusMessageList )
+ {
+ var rui = client.statusMessageList[ id ]
+ RuiSetGameTime( rui, "startFadeOutTime", Time() )
+
+ // Remove it from table
+ delete client.statusMessageList[ id ]
+
+ // Wait for animation
+ wait 0.6
+
+ RuiDestroyIfAlive( rui )
+
+ int i = 0
+ foreach( _id, _rui in client.statusMessageList )
+ {
+ RuiSetInt( _rui, "listPos", i )
+ i++
+ }
+ }
+}
+
+void function EditStatusMessage( string id )
+{
+ if( id in client.statusMessageList )
+ {
+ var rui = client.statusMessageList[ id ]
+ RuiSetString( rui, "titleText", client.temp.title )
+ RuiSetString( rui, "itemText", client.temp.description )
+ }
+}
+
+void function CreateStatusMessage( string id )
+{
+ // Cap at 4 messages at a time
+ if( client.statusMessageList.len() == STATUS_MESSAGES_MAX )
+ return
+
+ var rui = CreatePermanentCockpitRui( $"ui/at_wave_intro.rpak" )
+ RuiSetInt( rui, "listPos", client.statusMessageList.len() )
+ RuiSetGameTime( rui, "startFadeInTime", Time() )
+ RuiSetString( rui, "titleText", client.temp.title )
+ RuiSetString( rui, "itemText", client.temp.description )
+ RuiSetFloat2( rui, "offset", < 0, -250, 0 > )
+
+ client.statusMessageList[ id ] <- rui
+}
+
+void function SelectPollOption_Threaded( int index )
+{
+ if ( index >= client.poll.ruis.len() || index <= 0 )
+ return
+
+ RuiSetFloat3( client.poll.ruis[ index ], "msgColor", ColorSelected )
+ EmitSoundOnEntity( GetLocalClientPlayer(), "menu_accept" )
+
+ float endTime = 1 + client.poll.duration
+ while( endTime > Time() && client.poll.pollActive )
+ WaitFrame()
+
+ GetLocalClientPlayer().ClientCommand( "poll_respond " + index )
+
+ foreach( var rui in client.poll.ruis )
+ RuiDestroyIfAlive( rui )
+
+ client.poll.ruis.clear()
+ client.poll.pollActive = false
+}
+
+void function ShowPollMessage_Threaded()
+{
+ if( client.poll.pollActive )
+ return
+
+ client.poll.pollActive = true
+
+ for( int i = 0; i < client.poll.options.len() + 1; i++ )
+ {
+ var rui = CreateCockpitRui( $"ui/cockpit_console_text_top_left.rpak" )
+ // This makes it fade and me no likey >:(
+ RuiSetFloat2( rui, "msgPos", < 0, 0.4 + i * 0.025, 0 > )
+ if( i == 0 )
+ {
+ RuiSetFloat3( rui, "msgColor", ColorSelected )
+ RuiSetString( rui, "msgText", client.poll.header )
+ }
+ else
+ {
+ RuiSetFloat3( rui, "msgColor", ColorBase )
+ RuiSetString( rui, "msgText", i + ". " + client.poll.options[i - 1] )
+ }
+
+ RuiSetFloat( rui, "msgFontSize", 30.0 )
+ RuiSetFloat( rui, "msgAlpha", 0.9 )
+ RuiSetFloat( rui, "thicken", 0.0 )
+
+ client.poll.ruis.append( rui )
+ }
+
+ client.poll.options.clear()
+
+ float endTime = Time() + client.poll.duration
+ while( endTime > Time() && client.poll.pollActive )
+ WaitFrame()
+
+
+ foreach( var rui in client.poll.ruis )
+ RuiDestroyIfAlive( rui )
+
+ client.poll.ruis.clear()
+ client.poll.pollActive = false
+}
+
+void function InfoMessageHandler_Threaded()
+{
+ while( true )
+ {
+ while( client.infoMessageQueue.len() == 0 )
+ WaitFrame()
+
+ var rui = CreatePermanentCockpitRui( $"ui/death_hint_mp.rpak" )
+ RuiSetString( rui, "hintText", client.infoMessageQueue[0].description )
+ RuiSetGameTime( rui, "startTime", Time() )
+ RuiSetFloat3( rui, "bgColor", < 0, 0, 0 > )
+ RuiSetFloat( rui, "bgAlpha", 0.5 )
+
+ wait 7
+
+ client.infoMessageQueue.remove( 0 )
+ RuiDestroyIfAlive( rui )
+ }
+}
+
+void function AnnouncementMessageHandler_Threaded()
+{
+ while( true )
+ {
+ while( client.announcementQueue.len() == 0 )
+ WaitFrame()
+
+ AnnouncementData announcement = Announcement_Create( client.announcementQueue[0].title )
+ Announcement_SetSubText( announcement, client.announcementQueue[0].description )
+ Announcement_SetTitleColor( announcement, client.announcementQueue[0].color )
+ Announcement_SetPurge( announcement, true )
+ Announcement_SetPriority( announcement, client.announcementQueue[0].priority )
+ Announcement_SetSoundAlias( announcement, SFX_HUD_ANNOUNCE_QUICK )
+ Announcement_SetStyle( announcement, client.announcementQueue[0].style )
+ AnnouncementFromClass( GetLocalViewPlayer(), announcement )
+
+ wait 5
+
+ client.announcementQueue.remove(0)
+ }
+}
+
+void function LargeMessageHandler_Threaded()
+{
+ while( true )
+ {
+ while( client.largeMessageQueue.len() == 0 )
+ WaitFrame()
+
+ var rui = CreatePermanentCockpitRui( $"ui/fd_tutorial_tip.rpak" )
+ RuiSetImage( rui, "backgroundImage", StringToAsset( strip( client.largeMessageQueue[0].image ) ) )
+ RuiSetString( rui, "titleText", client.largeMessageQueue[0].title )
+ RuiSetString( rui, "descriptionText", client.largeMessageQueue[0].description )
+ RuiSetGameTime( rui, "updateTime", Time() )
+ RuiSetFloat( rui, "duration", client.largeMessageQueue[0].duration )
+
+ wait client.largeMessageQueue[0].duration
+
+ client.largeMessageQueue.remove(0)
+ RuiDestroyIfAlive( rui )
+ }
+}
+
+void function PopUpMessageHandler_Threaded()
+{
+ while( true )
+ {
+ while( client.popupMessageQueue.len() == 0 )
+ WaitFrame()
+
+ var rui = CreateCockpitRui( $"ui/killdeath_info.rpak" )
+ RuiSetGameTime( rui, "startTime", Time() )
+ RuiSetFloat( rui, "duration", 20 ) // It has a weird end animation
+ RuiSetString( rui, "messageText", client.popupMessageQueue[0].description )
+ RuiSetBool( rui, "isBigText", true )
+
+ wait 2.4
+
+ client.popupMessageQueue.remove(0)
+ RuiDestroyIfAlive( rui )
+ }
+}
+
+#endif // CLIENT \ No newline at end of file
diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_northstar_custom_precache.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_northstar_custom_precache.gnut
index 848a4b86..b8d4b1ba 100644
--- a/Northstar.Custom/mod/scripts/vscripts/sh_northstar_custom_precache.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/sh_northstar_custom_precache.gnut
@@ -1,13 +1,16 @@
-untyped
global function NorthstarCustomPrecache
void function NorthstarCustomPrecache()
{
PrecacheWeapon( "mp_weapon_peacekraber" )
PrecacheWeapon( "mp_titanweapon_triplethreat" )
+ PrecacheWeapon( "mp_titanweapon_arc_cannon" )
PrecacheWeapon( "melee_pilot_kunai" )
- // create kunai damage source so game won't crash when we hit smth with it
- // just using the default melee one, easier than making a new one
- getconsttable()[ "eDamageSourceId" ][ "melee_pilot_kunai" ] <- eDamageSourceId.melee_pilot_emptyhanded
+ RegisterWeaponDamageSources(
+ {
+ mp_weapon_peacekraber = "#WPN_PEACEKRABER",
+ melee_pilot_kunai = "#MELEE_KUNAI"
+ }
+ )
}
diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_northstar_http_requests.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_northstar_http_requests.gnut
new file mode 100644
index 00000000..8ff55eae
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/sh_northstar_http_requests.gnut
@@ -0,0 +1,235 @@
+globalize_all_functions
+
+global enum HttpRequestMethod
+{
+ GET = 0,
+ POST = 1
+ HEAD = 2,
+ PUT = 3,
+ DELETE = 4,
+ PATCH = 5,
+ OPTIONS = 6,
+}
+
+global struct HttpRequest
+{
+ /** Method used for this http request. */
+ int method
+ /** Base URL of this http request. */
+ string url
+ /** Headers used for this http request. Some may get overridden or ignored. */
+ table< string, array< string > > headers
+ /** Query parameters for this http request. */
+ table< string, array< string > > queryParameters
+ /** The content type of this http request. Defaults to application/json & UTF-8 charset. */
+ string contentType = "application/json; charset=utf-8"
+ /** The body of this http request. If set, will override queryParameters.*/
+ string body
+ /** The timeout for the http request in seconds. Must be between 1 and 60. */
+ int timeout = 60
+ /** If set, the override to use for the User-Agent header. */
+ string userAgent
+}
+
+global struct HttpRequestResponse
+{
+ /** The status code returned by the remote the call was made to. */
+ int statusCode
+ /** The body of the response. */
+ string body
+ /** The raw headers returned by the remote. */
+ string rawHeaders
+ /** A key -> values table of headers returned by the remote. */
+ table< string, array< string > > headers
+}
+
+global struct HttpRequestFailure
+{
+ /** The error code returned by native for this failure. */
+ int errorCode
+ /** The reason why this http request failed. */
+ string errorMessage
+}
+
+struct HttpRequestCallbacks
+{
+ /**
+ * The function to call if the HTTP request was a success.
+ * Passes in the response received from the remote.
+ */
+ void functionref( HttpRequestResponse ) onSuccess
+
+ /**
+ * The function to call if the HTTP request failed.
+ */
+ void functionref( HttpRequestFailure ) onFailure
+}
+
+table< int, HttpRequestCallbacks > pendingCallbacks
+
+/**
+ * Called from native when a HTTP request is successful.
+ * This is internal and shouldn't be used.
+ * Keep in mind that the success can be successful, but have a non-success status code.
+ * @param handle The handle of the request we got a response for.
+ * @param statusCode The status code returned in the response.
+ * @param body The body returned for GET requests.
+ * @param headers The headers that were returned in the response.
+ */
+void function NSHandleSuccessfulHttpRequest( int handle, int statusCode, string body, string headers )
+{
+ if ( !( handle in pendingCallbacks ) )
+ {
+ return
+ }
+
+ if ( pendingCallbacks[ handle ].onSuccess != null )
+ {
+ HttpRequestResponse response
+ response.statusCode = statusCode
+ response.body = body
+ response.rawHeaders = headers
+
+ // Parse the raw headers into key -> values
+ array<string> values = split( headers, "\n" )
+
+ foreach ( string header in values )
+ {
+ var index = header.find( ":" )
+ if ( index == null )
+ {
+ continue
+ }
+
+ expect int( index )
+
+ string name = strip( header.slice( 0, index ) )
+ string value = strip( header.slice( index + 1 ) )
+
+ if ( name in response.headers )
+ {
+ response.headers[ name ].append( value )
+ }
+ else
+ {
+ response.headers[ name ] <- [ value ]
+ }
+ }
+
+ pendingCallbacks[ handle ].onSuccess( response )
+ }
+
+ delete pendingCallbacks[ handle ]
+}
+
+/**
+ * Called from native when a HTTP request has failed.
+ * This is internal and shouldn't be used.
+ * @param handle The handle of the request that failed.
+ * @param errorCode The error code returned by curl.
+ * @param errorMessage The error message returned by curl.
+ */
+void function NSHandleFailedHttpRequest( int handle, int errorCode, string errorMessage )
+{
+ if ( handle in pendingCallbacks )
+ {
+ if ( pendingCallbacks[ handle ].onFailure != null )
+ {
+ HttpRequestFailure failure
+ failure.errorCode = errorCode
+ failure.errorMessage = errorMessage
+
+ pendingCallbacks[ handle ].onFailure( failure )
+ }
+
+ delete pendingCallbacks[ handle ]
+ }
+}
+
+/**
+ * Launch a HTTP request with the given request data.
+ * This function is async, and the provided callbacks will be called when it is completed.
+ * @param requestParameters The parameters to use for this request.
+ * @param onSuccess The callback to execute if the request is successful.
+ * @param onFailure The callback to execute if the request has failed.
+ * @returns Whether or not the request has been successfully started.
+ */
+bool function NSHttpRequest( HttpRequest requestParameters, void functionref( HttpRequestResponse ) onSuccess = null, void functionref( HttpRequestFailure ) onFailure = null )
+{
+ int handle = NS_InternalMakeHttpRequest( requestParameters.method, requestParameters.url, requestParameters.headers,
+ requestParameters.queryParameters, requestParameters.contentType, requestParameters.body, requestParameters.timeout, requestParameters.userAgent )
+
+ if ( handle != -1 && ( onSuccess != null || onFailure != null ) )
+ {
+ HttpRequestCallbacks callback
+ callback.onSuccess = onSuccess
+ callback.onFailure = onFailure
+
+ pendingCallbacks[ handle ] <- callback
+ }
+
+ return handle != -1
+}
+
+/**
+ * Launches an HTTP GET request at the specified URL with the given query parameters.
+ * This function is async, and the provided callbacks will be called when it is completed.
+ * @param url The url to make the http request for.
+ * @param queryParameters A table of key value parameters to insert in the url.
+ * @param onSuccess The callback to execute if the request is successful.
+ * @param onFailure The callback to execute if the request has failed.
+ * @returns Whether or not the request has been successfully started.
+ */
+bool function NSHttpGet( string url, table< string, array< string > > queryParameters = {}, void functionref( HttpRequestResponse ) onSuccess = null, void functionref( HttpRequestFailure ) onFailure = null )
+{
+ HttpRequest request
+ request.method = HttpRequestMethod.GET
+ request.url = url
+ request.queryParameters = queryParameters
+
+ return NSHttpRequest( request, onSuccess, onFailure )
+}
+
+/**
+ * Launches an HTTP POST request at the specified URL with the given query parameters.
+ * This function is async, and the provided callbacks will be called when it is completed.
+ * @param url The url to make the http request for.
+ * @param queryParameters A table of key value parameters to insert in the url.
+ * @param onSuccess The callback to execute if the request is successful.
+ * @param onFailure The callback to execute if the request has failed.
+ * @returns Whether or not the request has been successfully started.
+ */
+bool function NSHttpPostQuery( string url, table< string, array< string > > queryParameters, void functionref( HttpRequestResponse ) onSuccess = null, void functionref( HttpRequestFailure ) onFailure = null )
+{
+ HttpRequest request
+ request.method = HttpRequestMethod.POST
+ request.url = url
+ request.queryParameters = queryParameters
+
+ return NSHttpRequest( request, onSuccess, onFailure )
+}
+
+/**
+ * Launches an HTTP POST request at the specified URL with the given body.
+ * This function is async, and the provided callbacks will be called when it is completed.
+ * @param url The url to make the http request for.
+ * @param queryParameters A table of key value parameters to insert in the url.
+ * @param onSuccess The callback to execute if the request is successful.
+ * @param onFailure The callback to execute if the request has failed.
+ * @returns Whether or not the request has been successfully started.
+ */
+bool function NSHttpPostBody( string url, string body, void functionref( HttpRequestResponse ) onSuccess = null, void functionref( HttpRequestFailure ) onFailure = null )
+{
+ HttpRequest request
+ request.method = HttpRequestMethod.POST
+ request.url = url
+ request.body = body
+
+ return NSHttpRequest( request, onSuccess, onFailure )
+}
+
+/** Whether or not the given status code is considered successful. */
+bool function NSIsSuccessHttpCode( int statusCode )
+{
+ return statusCode >= 200 && statusCode <= 299
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_northstar_safe_io.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_northstar_safe_io.gnut
new file mode 100644
index 00000000..f7b31cc2
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/sh_northstar_safe_io.gnut
@@ -0,0 +1,83 @@
+globalize_all_functions
+
+table< int, void functionref( string ) > pendingCallbacks
+table< int, void functionref( table ) > pendingJSONCallbacks
+table< int, void functionref() > failedCallbacks
+
+
+void function NSLoadFile( string file, void functionref( string ) onSuccess, void functionref() onFailure = null )
+{
+ int handle = NS_InternalLoadFile( file )
+
+ pendingCallbacks[handle] <- onSuccess
+ if (onFailure != null)
+ failedCallbacks[handle] <- onFailure
+}
+
+void function NSLoadJSONFile( string file, void functionref( table ) onSuccess, void functionref() onFailure = null )
+{
+ int handle = NS_InternalLoadFile( file )
+
+ pendingJSONCallbacks[handle] <- onSuccess
+ if (onFailure != null)
+ failedCallbacks[handle] <- onFailure
+}
+
+void function NSHandleLoadResult( int handle, bool success, string result )
+{
+ bool hasFailedCallback = handle in failedCallbacks
+ bool isJSONRequest = handle in pendingJSONCallbacks
+ bool isValid = isJSONRequest || handle in pendingCallbacks
+
+ if (!isValid)
+ throw "Invalid IO callback handle"
+
+ if (success)
+ {
+ if (isJSONRequest)
+ {
+ try
+ {
+ table result = DecodeJSON(result, true)
+ pendingJSONCallbacks[handle](result)
+ }
+ catch (ex)
+ {
+ print(ex)
+ // parsing failed, setting 'success' to false, since we
+ // consider this a failure.
+ success = false
+ }
+ }
+ else
+ {
+ pendingCallbacks[handle](result)
+ }
+ }
+ // don't use 'else', json might fail parsing and set 'success' to false.
+ if (!success)
+ {
+ if (hasFailedCallback)
+ failedCallbacks[handle]()
+ else
+ {
+ if (isJSONRequest)
+ pendingJSONCallbacks[handle]({})
+ else
+ pendingCallbacks[handle]("")
+ }
+ }
+
+ if (isJSONRequest)
+ delete pendingJSONCallbacks[handle]
+ else
+ delete pendingCallbacks[handle]
+
+ if (hasFailedCallback)
+ delete failedCallbacks[handle]
+}
+
+array<string> function NSGetAllFiles( string path = "" )
+{
+ return NS_InternalGetAllFiles(path)
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan.gnut b/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan.gnut
index 92b4924b..814e4430 100644
--- a/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan.gnut
+++ b/Northstar.Custom/mod/scripts/vscripts/titan/sh_titan.gnut
@@ -219,7 +219,7 @@ void function AddArmBadgeToTitan( entity soul )
void function AddArmBadgeToTitan_Internal( entity soul )
{
- soul.EndSignal( "OnDeath" )
+ soul.EndSignal( "OnDestroy" )
// wait until the end of the frame to allow the soul to become owned by a boss player
WaitEndFrame()
diff --git a/Northstar.Custom/mod/scripts/vscripts/ui/ns_custom_mod_settings.gnut b/Northstar.Custom/mod/scripts/vscripts/ui/ns_custom_mod_settings.gnut
new file mode 100644
index 00000000..5a7d80b7
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/ui/ns_custom_mod_settings.gnut
@@ -0,0 +1,8 @@
+global function NSCustomModSettings
+
+void function NSCustomModSettings()
+{
+ ModSettings_AddModTitle( "Northstar Custom" , 2 )
+ ModSettings_AddModCategory( "Event Models" )
+ ModSettings_AddEnumSetting( "ns_show_event_models", "Show Event Models", [ "#SETTING_OFF", "#SETTING_ON" ], 2 )
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut
new file mode 100644
index 00000000..defb1a56
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/weapons/_arc_cannon.nut
@@ -0,0 +1,1033 @@
+untyped
+
+global function ArcCannon_Init
+
+global function ArcCannon_PrecacheFX
+global function ArcCannon_Start
+global function ArcCannon_Stop
+global function ArcCannon_ChargeBegin
+global function ArcCannon_ChargeEnd
+global function FireArcCannon
+global function ArcCannon_HideIdleEffect
+#if SERVER
+ global function AddToArcCannonTargets
+ global function RemoveArcCannonTarget
+ global function ConvertTitanShieldIntoBonusCharge
+#endif
+global function GetArcCannonChargeFraction
+
+global function IsEntANeutralMegaTurret
+global function CreateArcCannonBeam
+
+
+// Aiming & Range
+global const DEFAULT_ARC_CANNON_FOVDOT = 0.98 // First target must be within this dot to be zapped and start a chain
+global const DEFAULT_ARC_CANNON_FOVDOT_MISSILE = 0.95 // First target must be within this dot to be zapped and start a chain ( if it's a missile, we allow more leaniency )
+global const ARC_CANNON_RANGE_CHAIN = 400 // Max distance we can arc from one target to another
+global const ARC_CANNON_TITAN_RANGE_CHAIN = 900 // Max distance we can arc from one target to another
+global const ARC_CANNON_CHAIN_COUNT_MIN = 5 // Max number of chains at no charge
+global const ARC_CANNON_CHAIN_COUNT_MAX = 5 // Max number of chains at full charge
+global const ARC_CANNON_CHAIN_COUNT_NPC = 2 // Number of chains when an NPC fires the weapon
+global const ARC_CANNON_FORK_COUNT_MAX = 1 // Number of forks that can come out of one target to other targets
+global const ARC_CANNON_FORK_DELAY = 0.1
+
+global const ARC_CANNON_RANGE_CHAIN_BURN = 400
+global const ARC_CANNON_TITAN_RANGE_CHAIN_BURN = 900
+global const ARC_CANNON_CHAIN_COUNT_MIN_BURN = 100 // Max number of chains at no charge
+global const ARC_CANNON_CHAIN_COUNT_MAX_BURN = 100 // Max number of chains at full charge
+global const ARC_CANNON_CHAIN_COUNT_NPC_BURN = 10 // Number of chains when an NPC fires the weapon
+global const ARC_CANNON_FORK_COUNT_MAX_BURN = 10 // Number of forks that can come out of one target to other targets
+global const ARC_CANNON_BEAM_LIFETIME_BURN = 1
+
+// Visual settings
+global const ARC_CANNON_BOLT_RADIUS_MIN = 32 // Bolt radius at no charge ( not actually sure what this does to the beam lol )
+global const ARC_CANNON_BOLT_RADIUS_MAX = 640 // Bold radius at full charge ( not actually sure what this does to the beam lol )
+global const ARC_CANNON_BOLT_WIDTH_MIN = 1 // Bolt width at no charge
+global const ARC_CANNON_BOLT_WIDTH_MAX = 26 // Bolt width at full charge
+global const ARC_CANNON_BOLT_WIDTH_NPC = 8 // Bolt width when used by NPC
+global const ARC_CANNON_BEAM_COLOR = "150 190 255"
+global const ARC_CANNON_BEAM_LIFETIME = 0.75
+
+// Player Effects
+global const ARC_CANNON_TITAN_SCREEN_SFX = "Null_Remove_SoundHook"
+global const ARC_CANNON_PILOT_SCREEN_SFX = "Null_Remove_SoundHook"
+global const ARC_CANNON_EMP_DURATION_MIN = 0.1
+global const ARC_CANNON_EMP_DURATION_MAX = 1.8
+global const ARC_CANNON_EMP_FADEOUT_DURATION = 0.4
+global const ARC_CANNON_SCREEN_EFFECTS_MIN = 0.01
+global const ARC_CANNON_SCREEN_EFFECTS_MAX = 0.02
+global const ARC_CANNON_SCREEN_THRESHOLD = 0.3385
+global const ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION = 0.2
+
+// Damage
+global const ARC_CANNON_DAMAGE_FALLOFF_SCALER = 0.75 // Amount of damage carried on to the next target in the chain lightning. If 0.75, then a target that would normally take 100 damage will take 75 damage if they are one chain deep, or 56 damage if 2 levels deep
+global const ARC_CANNON_DAMAGE_CHARGE_RATIO = 0.85 // What amount of charge is required for full damage.
+global const ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN = 0.676 // What amount of charge is required for full damage.
+global const ARC_CANNON_CAPACITOR_CHARGE_RATIO = 1.0
+
+// Options
+global const ARC_CANNON_TARGETS_MISSILES = 1 // 1 = arc cannon zaps missiles that are active, 0 = missiles are ignored by arc cannon
+
+//Mods
+global const OVERCHARGE_MAX_SHIELD_DECAY = 0.2
+global const OVERCHARGE_SHIELD_DECAY_MULTIPLIER = 0.04
+global const OVERCHARGE_BONUS_CHARGE_FRACTION = 0.05
+
+global const SPLITTER_DAMAGE_FALLOFF_SCALER = 0.6
+global const SPLITTER_FORK_COUNT_MAX = 10
+
+global const ARC_CANNON_SIGNAL_DEACTIVATED = "ArcCannonDeactivated"
+global const ARC_CANNON_SIGNAL_CHARGEEND = "ArcCannonChargeEnd"
+
+global const ARC_CANNON_BEAM_EFFECT = $"wpn_arc_cannon_beam"
+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,
+}
+
+struct {
+ array<string> missileCheckTargetnames = [
+ // "Arc Pylon",
+ "Arc Ball"
+ ]
+} file;
+
+function ArcCannon_Init()
+{
+ RegisterSignal( ARC_CANNON_SIGNAL_DEACTIVATED )
+ RegisterSignal( ARC_CANNON_SIGNAL_CHARGEEND )
+ PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT )
+ PrecacheParticleSystem( ARC_CANNON_BEAM_EFFECT_MOD )
+ PrecacheImpactEffectTable( ARC_CANNON_FX_TABLE )
+
+ #if CLIENT
+ AddDestroyCallback( "mp_titanweapon_arc_cannon", ClientDestroyCallback_ArcCannon_Stop )
+ #else
+ level._arcCannonTargetsArrayID <- CreateScriptManagedEntArray()
+ #endif
+
+ PrecacheParticleSystem( $"impact_arc_cannon_titan" )
+}
+
+function ArcCannon_PrecacheFX()
+{
+ PrecacheParticleSystem( $"wpn_arc_cannon_electricity_fp" )
+ PrecacheParticleSystem( $"wpn_arc_cannon_electricity" )
+
+ PrecacheParticleSystem( $"wpn_muzzleflash_arc_cannon_fp" )
+ PrecacheParticleSystem( $"wpn_muzzleflash_arc_cannon" )
+}
+
+function ArcCannon_Start( weapon )
+{
+ expect entity( weapon )
+ if ( !IsPilot( weapon.GetWeaponOwner() ) )
+ {
+ weapon.PlayWeaponEffectNoCull( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity", "muzzle_flash" )
+ weapon.EmitWeaponSound( "arc_cannon_charged_loop" )
+ }
+ else
+ {
+ weapon.EmitWeaponSound_1p3p( "Arc_Rifle_charged_Loop_1P", "Arc_Rifle_charged_Loop_3P" )
+ }
+}
+
+function ArcCannon_Stop( weapon, player = null )
+{
+ expect entity( weapon )
+ weapon.Signal( ARC_CANNON_SIGNAL_DEACTIVATED )
+
+ weapon.StopWeaponEffect( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity" )
+ weapon.StopWeaponSound( "arc_cannon_charged_loop" )
+}
+
+function ArcCannon_ChargeBegin( entity weapon )
+{
+ #if SERVER
+ if ( weapon.HasMod( "overcharge" ) )
+ {
+ entity weaponOwner = weapon.GetWeaponOwner()
+ if ( weaponOwner.IsTitan() )
+ {
+ entity soul = weaponOwner.GetTitanSoul()
+ thread ConvertTitanShieldIntoBonusCharge( soul, weapon )
+ }
+ }
+ #endif
+
+ #if CLIENT
+ if ( !weapon.ShouldPredictProjectiles() )
+ return
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ Assert( weaponOwner.IsPlayer() )
+ weaponOwner.StartArcCannon();
+ #endif
+}
+
+function ArcCannon_ChargeEnd( entity weapon, entity player = null )
+{
+ #if SERVER
+ if ( IsValid( weapon ) )
+ weapon.Signal( ARC_CANNON_SIGNAL_CHARGEEND )
+ #endif
+
+ #if CLIENT
+ if ( weapon.GetWeaponOwner() == GetLocalViewPlayer() )
+ {
+ entity weaponOwner
+ if ( player != null )
+ weaponOwner = player
+ else
+ weaponOwner = weapon.GetWeaponOwner()
+
+ if ( IsValid( weaponOwner ) && weaponOwner.IsPlayer() )
+ weaponOwner.StopArcCannon()
+ }
+ #endif
+}
+
+#if SERVER
+function ConvertTitanShieldIntoBonusCharge( entity soul, entity weapon )
+{
+ weapon.EndSignal( ARC_CANNON_SIGNAL_CHARGEEND )
+ weapon.EndSignal( "OnDestroy" )
+
+ local maxShieldDecay = OVERCHARGE_MAX_SHIELD_DECAY
+ local bonusChargeFraction = OVERCHARGE_BONUS_CHARGE_FRACTION
+ local shieldDecayMultiplier = OVERCHARGE_SHIELD_DECAY_MULTIPLIER
+ int shieldHealthMax = soul.GetShieldHealthMax()
+ local chargeRatio = GetArcCannonChargeFraction( weapon )
+
+ while( 1 )
+ {
+ if ( !IsValid( soul ) || !IsValid( weapon ) )
+ break
+
+ local baseCharge = GetWeaponChargeFrac( weapon ) // + GetOverchargeBonusChargeFraction()
+ local charge = clamp ( baseCharge * ( 1 / chargeRatio ), 0.0, 1.0 )
+ if ( charge < 1.0 || maxShieldDecay > 0)
+ {
+ int shieldHealth = soul.GetShieldHealth()
+
+ //Slight inconsistency in server updates, this ensures it never takes too much.
+ if ( shieldDecayMultiplier > maxShieldDecay )
+ shieldDecayMultiplier = maxShieldDecay
+ maxShieldDecay -= shieldDecayMultiplier
+
+ local shieldDecayAmount = shieldHealthMax * shieldDecayMultiplier
+ local newShieldAmount = shieldHealth - shieldDecayAmount
+ soul.SetShieldHealth( max( newShieldAmount, 0 ) )
+ soul.nextRegenTime = Time() + GetShieldRegenTime( soul )
+
+ if ( shieldDecayAmount > shieldHealth )
+ bonusChargeFraction = bonusChargeFraction * ( shieldHealth / shieldDecayAmount )
+ weapon.SetWeaponChargeFraction( baseCharge + bonusChargeFraction )
+ }
+ wait 0.1
+ }
+}
+#endif
+
+function FireArcCannon( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ local weaponScriptScope = weapon.GetScriptScope()
+ local baseCharge = GetWeaponChargeFrac( weapon ) // + GetOverchargeBonusChargeFraction()
+ local charge = clamp( baseCharge * ( 1 / GetArcCannonChargeFraction( weapon ) ), 0.0, 1.0 )
+ float newVolume = GraphCapped( charge, 0.25, 1.0, 0.0, 1.0 )
+
+ weapon.EmitWeaponNpcSound( LOUD_WEAPON_AI_SOUND_RADIUS_MP, 0.2 )
+
+ weapon.PlayWeaponEffect( $"wpn_muzzleflash_arc_cannon_fp", $"wpn_muzzleflash_arc_cannon", "muzzle_flash" )
+
+ local attachmentName = "muzzle_flash"
+ local attachmentIndex = weapon.LookupAttachment( attachmentName )
+ Assert( attachmentIndex >= 0 )
+ local muzzleOrigin = weapon.GetAttachmentOrigin( attachmentIndex )
+
+ //printt( "-------- FIRING ARC CANNON --------" )
+
+ table firstTargetInfo = GetFirstArcCannonTarget( weapon, attackParams )
+ if ( !IsValid( firstTargetInfo.target ) )
+ FireArcNoTargets( weapon, attackParams, muzzleOrigin )
+ else
+ FireArcWithTargets( weapon, firstTargetInfo, attackParams, muzzleOrigin )
+
+ return 1
+}
+
+table function GetFirstArcCannonTarget( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ entity owner = weapon.GetWeaponOwner()
+ local coneHeight = weapon.GetMaxDamageFarDist()
+
+ local angleToAxis = 2 // set this too high and auto-titans using it will error on FindVisibleEntitiesInCone
+ array<entity> ignoredEntities = [ owner, weapon ]
+ int traceMask = TRACE_MASK_SHOT
+ int flags = VIS_CONE_ENTS_TEST_HITBOXES
+ local antilagPlayer = null
+ if ( owner.IsPlayer() )
+ {
+ angleToAxis = owner.GetAttackSpreadAngle() * 0.11
+ antilagPlayer = owner
+ }
+
+ int ownerTeam = owner.GetTeam()
+
+ // Get a missile target and a non-missile target in the cone that the player can zap
+ // We do this in a separate check so we can use a wider cone to be more forgiving for targeting missiles
+ table firstTargetInfo = {}
+ firstTargetInfo.target <- null
+ firstTargetInfo.hitLocation <- null
+
+ for ( int i = 0; i < 2; i++ )
+ {
+ local missileCheck = i == 0
+ local coneAngle = angleToAxis
+ if ( missileCheck && owner.IsPlayer() ) // missile check only if owner is player
+ coneAngle *= 8.0
+
+ coneAngle = clamp( coneAngle, 0.1, 89.9 )
+
+ array<VisibleEntityInCone> results = FindVisibleEntitiesInCone( attackParams.pos, attackParams.dir, coneHeight, coneAngle, ignoredEntities, traceMask, flags, antilagPlayer )
+ foreach ( result in results )
+ {
+ entity visibleEnt = result.ent
+
+ if ( !IsValid( visibleEnt ) )
+ continue
+
+ if ( visibleEnt.IsPhaseShifted() )
+ continue
+
+ local classname = IsServer() ? visibleEnt.GetClassName() : visibleEnt.GetSignifierName()
+
+ if ( !( classname in ArcCannonTargetClassnames ) )
+ continue
+
+ if ( "GetTeam" in visibleEnt )
+ {
+ int visibleEntTeam = visibleEnt.GetTeam()
+ if ( visibleEntTeam == ownerTeam )
+ continue
+ if ( IsEntANeutralMegaTurret( visibleEnt, ownerTeam ) )
+ continue
+ }
+
+ expect string( classname )
+ string targetname = visibleEnt.GetTargetName()
+
+ if ( missileCheck && ( classname != "rpg_missile" && !file.missileCheckTargetnames.contains( targetname ) ) )
+ continue
+
+ if ( !missileCheck && ( classname == "rpg_missile" || file.missileCheckTargetnames.contains( targetname ) ) )
+ continue
+
+ firstTargetInfo.target = visibleEnt
+ firstTargetInfo.hitLocation = result.visiblePosition
+ break
+ }
+ }
+ //Creating a whiz-by sound.
+ weapon.FireWeaponBullet_Special( attackParams.pos, attackParams.dir, 1, 0, true, true, true, true, true, false, false )
+
+ return firstTargetInfo
+}
+
+function FireArcNoTargets( entity weapon, WeaponPrimaryAttackParams attackParams, muzzleOrigin )
+{
+ Assert( IsValid( weapon ) )
+ entity player = weapon.GetWeaponOwner()
+ local chargeFrac = GetWeaponChargeFrac( weapon )
+ local beamVec = attackParams.dir * weapon.GetMaxDamageFarDist()
+ local playerEyePos = player.EyePosition()
+ TraceResults traceResults = TraceLineHighDetail( playerEyePos, (playerEyePos + beamVec), weapon, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ local beamEnd = traceResults.endPos
+
+ VortexBulletHit ornull vortexHit = VortexBulletHitCheck( player, playerEyePos, beamEnd )
+ if ( vortexHit )
+ {
+ expect VortexBulletHit( vortexHit )
+ #if SERVER
+ entity vortexWeapon = vortexHit.vortex.GetOwnerWeapon()
+ string className = IsValid( vortexWeapon ) ? vortexWeapon.GetWeaponClassName() : ""
+ if ( vortexWeapon && ( className == "mp_titanweapon_vortex_shield" || className == "mp_titanweapon_vortex_shield_ion" ) )
+ {
+ float amount = expect float ( chargeFrac ) * weapon.GetWeaponSettingFloat( eWeaponVar.vortex_drain )
+ if ( amount <= 0.0 )
+ return
+
+ if ( vortexWeapon.GetWeaponClassName() == "mp_titanweapon_vortex_shield_ion" )
+ {
+ entity owner = vortexWeapon.GetWeaponOwner()
+ int totalEnergy = owner.GetSharedEnergyTotal()
+ owner.TakeSharedEnergy( int( float( totalEnergy ) * amount ) )
+ }
+ else
+ {
+ float frac = min ( vortexWeapon.GetWeaponChargeFraction() + amount, 1.0 )
+ vortexWeapon.SetWeaponChargeFraction( frac )
+ }
+ }
+ else if ( IsVortexSphere( vortexHit.vortex ) )
+ {
+ // do damage to vortex_sphere entities that isn't the titan "vortex shield"
+ local damageNear = weapon.GetWeaponInfoFileKeyField( "damage_near_value" )
+ local damage = damageNear * GraphCapped( chargeFrac, 0, 1, 0.0, 1.0 ) * 10 // do more damage the more charged the weapon is.
+ VortexSphereDrainHealthForDamage( vortexHit.vortex, damage )
+ if ( IsValid( player ) && player.IsPlayer() )
+ player.NotifyDidDamage( vortexHit.vortex, 0, vortexHit.hitPos, 0, damage, DF_NO_HITBEEP, 0, null, 0 )
+ }
+ #endif
+ beamEnd = vortexHit.hitPos
+ }
+
+ float radius = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_RADIUS_MIN, ARC_CANNON_BOLT_RADIUS_MAX )
+ local boltWidth = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_WIDTH_MIN, ARC_CANNON_BOLT_WIDTH_MAX )
+ if ( player.IsNPC() )
+ boltWidth = ARC_CANNON_BOLT_WIDTH_NPC
+ thread CreateArcCannonBeam( weapon, null, muzzleOrigin, beamEnd, player, ARC_CANNON_BEAM_LIFETIME, radius, boltWidth, 2, false, true )
+
+ #if SERVER
+ PlayImpactFXTable( expect vector( beamEnd ), player, ARC_CANNON_FX_TABLE, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+ #endif
+}
+
+function FireArcWithTargets( entity weapon, table firstTargetInfo, WeaponPrimaryAttackParams attackParams, muzzleOrigin )
+{
+ local beamStart = muzzleOrigin
+ local beamEnd
+ entity player = weapon.GetWeaponOwner()
+ local chargeFrac = GetWeaponChargeFrac( weapon )
+ float radius = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_RADIUS_MIN, ARC_CANNON_BOLT_RADIUS_MAX )
+ float boltWidth = Graph( chargeFrac, 0, 1, ARC_CANNON_BOLT_WIDTH_MIN, ARC_CANNON_BOLT_WIDTH_MAX )
+ local maxChains
+ local minChains
+
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ {
+ if ( player.IsNPC() )
+ maxChains = ARC_CANNON_CHAIN_COUNT_NPC_BURN
+ else
+ maxChains = ARC_CANNON_CHAIN_COUNT_MAX_BURN
+
+ minChains = ARC_CANNON_CHAIN_COUNT_MIN_BURN
+ }
+ else
+ {
+ if ( player.IsNPC() )
+ maxChains = ARC_CANNON_CHAIN_COUNT_NPC
+ else
+ maxChains = ARC_CANNON_CHAIN_COUNT_MAX
+
+ minChains = ARC_CANNON_CHAIN_COUNT_MIN
+ }
+
+ if ( !player.IsNPC() )
+ maxChains = Graph( chargeFrac, 0, 1, minChains, maxChains )
+
+ table zapInfo = {}
+ zapInfo.weapon <- weapon
+ zapInfo.player <- player
+ zapInfo.muzzleOrigin <- muzzleOrigin
+ zapInfo.radius <- radius
+ zapInfo.boltWidth <- boltWidth
+ zapInfo.maxChains <- maxChains
+ zapInfo.chargeFrac <- chargeFrac
+ zapInfo.zappedTargets <- {}
+ zapInfo.zappedTargets[ firstTargetInfo.target ] <- true
+ zapInfo.dmgSourceID <- weapon.GetDamageSourceID()
+ local chainNum = 1
+ thread ZapTargetRecursive( expect entity( firstTargetInfo.target), zapInfo, zapInfo.muzzleOrigin, expect vector( firstTargetInfo.hitLocation ), chainNum )
+}
+
+function ZapTargetRecursive( entity target, table zapInfo, beamStartPos, vector ornull firstTargetBeamEndPos = null, chainNum = 1 )
+{
+ if ( !IsValid( target ) )
+ return
+
+ if ( !IsValid( zapInfo.weapon ) )
+ return
+
+ Assert( target in zapInfo.zappedTargets )
+ if ( chainNum > zapInfo.maxChains )
+ return
+ vector beamEndPos
+ if ( firstTargetBeamEndPos == null )
+ beamEndPos = target.GetWorldSpaceCenter()
+ else
+ beamEndPos = expect vector( firstTargetBeamEndPos )
+
+ waitthread ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum )
+
+ // Get other nearby targets we can chain to
+ #if SERVER
+ if ( !IsValid( zapInfo.weapon ) )
+ return
+
+ var noArcing = expect entity( zapInfo.weapon ).GetWeaponInfoFileKeyField( "disable_arc" )
+
+ if ( noArcing != null && noArcing == 1 )
+ return // no chaining on new arc cannon
+
+ // NOTE: 'target' could be invalid at this point (no corpse)
+ array<entity> chainTargets = GetArcCannonChainTargets( beamEndPos, target, zapInfo )
+ foreach( entity chainTarget in chainTargets )
+ {
+ local newChainNum = chainNum
+ if ( chainTarget.GetClassName() != "rpg_missile" )
+ newChainNum++
+ zapInfo.zappedTargets[ chainTarget ] <- true
+ thread ZapTargetRecursive( chainTarget, zapInfo, beamEndPos, null, newChainNum )
+ }
+
+ if ( IsValid( zapInfo.player ) && zapInfo.player.IsPlayer() && zapInfo.zappedTargets.len() >= 5 )
+ {
+ #if HAS_STATS
+ if ( chainNum == 5 )
+ UpdatePlayerStat( expect entity( zapInfo.player ), "misc_stats", "arcCannonMultiKills", 1 )
+ #endif
+ }
+ #endif
+}
+
+function ZapTarget( zapInfo, target, beamStartPos, beamEndPos, chainNum = 1 )
+{
+ expect entity( target )
+ expect vector( beamStartPos )
+ expect vector( beamEndPos )
+
+ //DebugDrawLine( beamStartPos, beamEndPos, 255, 0, 0, true, 5.0 )
+ local boltWidth = zapInfo.boltWidth
+ if ( zapInfo.player.IsNPC() )
+ boltWidth = ARC_CANNON_BOLT_WIDTH_NPC
+ local firstBeam = ( chainNum == 1 )
+ #if SERVER
+ if ( firstBeam )
+ {
+ PlayImpactFXTable( beamEndPos, expect entity( zapInfo.player ), ARC_CANNON_FX_TABLE, SF_ENVEXPLOSION_INCLUDE_ENTITIES )
+ }
+ #endif
+
+ thread CreateArcCannonBeam( zapInfo.weapon, target, beamStartPos, beamEndPos, zapInfo.player, ARC_CANNON_BEAM_LIFETIME, zapInfo.radius, boltWidth, 5, true, firstBeam )
+
+ #if SERVER
+ local deathPackage = damageTypes.arcCannon
+
+ float damageAmount
+ int damageMin
+ int damageMax
+
+ int damageFarValue = eWeaponVar.damage_far_value
+ int damageNearValue = eWeaponVar.damage_near_value
+ int damageFarValueTitanArmor = eWeaponVar.damage_far_value_titanarmor
+ int damageNearValueTitanArmor = eWeaponVar.damage_near_value_titanarmor
+ int damageFarDistance = eWeaponVar.damage_far_distance
+ int damageNearDistance = eWeaponVar.damage_near_distance
+ if ( zapInfo.player.IsNPC() )
+ {
+ damageFarValue = eWeaponVar.npc_damage_far_value
+ damageNearValue = eWeaponVar.npc_damage_near_value
+ damageFarValueTitanArmor = eWeaponVar.npc_damage_far_value_titanarmor
+ damageNearValueTitanArmor = eWeaponVar.npc_damage_near_value_titanarmor
+ damageFarDistance = eWeaponVar.npc_damage_far_distance
+ damageNearDistance = eWeaponVar.npc_damage_near_distance
+ }
+
+ if ( IsValid( target ) && IsValid( zapInfo.player ) )
+ {
+ bool hasFastPacitor = false
+ bool noArcing = false
+
+ entity weapon = expect entity( zapInfo.weapon )
+ hasFastPacitor = weapon.GetWeaponInfoFileKeyField( "push_apart" ) != null && weapon.GetWeaponInfoFileKeyField( "push_apart" ) == 1
+ noArcing = weapon.GetWeaponInfoFileKeyField( "no_arcing" ) != null && weapon.GetWeaponInfoFileKeyField( "no_arcing" ) == 1
+ float critScale = weapon.GetWeaponSettingFloat( eWeaponVar.critical_hit_damage_scale )
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ damageMin = weapon.GetWeaponSettingInt( damageFarValueTitanArmor )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValueTitanArmor )
+ }
+ else
+ {
+ damageMin = weapon.GetWeaponSettingInt( damageFarValue )
+ damageMax = weapon.GetWeaponSettingInt( damageNearValue )
+
+ if ( target.IsNPC() )
+ {
+ damageMin *= 3 // more powerful against NPC humans so they die easy
+ damageMax *= 3
+ }
+ }
+
+
+ local chargeRatio = GetArcCannonChargeFraction( weapon )
+ if ( !weapon.GetWeaponSettingBool( eWeaponVar.charge_require_input ) )
+ {
+ // use distance for damage if the weapon auto-fires
+ float nearDist = weapon.GetWeaponSettingFloat( damageNearDistance )
+ float farDist = weapon.GetWeaponSettingFloat( damageFarDistance )
+
+ float dist = Distance( weapon.GetOrigin(), target.GetOrigin() )
+ damageAmount = GraphCapped( dist, farDist, nearDist, damageMin, damageMax )
+ }
+ else
+ {
+ // Scale damage amount based on how many chains deep we are
+ damageAmount = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, damageMin, damageMax )
+ }
+ local damageFalloff = ARC_CANNON_DAMAGE_FALLOFF_SCALER
+ if ( weapon.HasMod( "splitter" ) )
+ damageFalloff = SPLITTER_DAMAGE_FALLOFF_SCALER
+ damageAmount *= pow( damageFalloff, chainNum - 1 )
+
+ local isMissile = ( target.GetClassName() == "rpg_missile" )
+ if ( !isMissile )
+ wait ARC_CANNON_FORK_DELAY
+ else
+ wait 0.05
+
+ if ( !IsValid( target ) || !IsValid( zapInfo.player ) )
+ return
+
+ local dmgSourceID = zapInfo.dmgSourceID
+
+ // Update Later - This shouldn't be done here, this is not where we determine if damage actually happened to the target
+ // move to Damaged callback instead
+ if ( damageAmount > 0 )
+ {
+ float empDuration = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_EMP_DURATION_MIN, ARC_CANNON_EMP_DURATION_MAX )
+
+ if ( target.IsPlayer() && target.IsTitan() && !hasFastPacitor && !noArcing )
+ {
+ float empViewStrength = GraphCapped( zapInfo.chargeFrac, 0, chargeRatio, ARC_CANNON_SCREEN_EFFECTS_MIN, ARC_CANNON_SCREEN_EFFECTS_MAX )
+
+ if ( target.IsTitan() && zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD )
+ {
+ Remote_CallFunction_Replay( target, "ServerCallback_TitanEMP", empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION )
+ EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_TITAN_SCREEN_SFX )
+ }
+ else if ( zapInfo.chargeFrac >= ARC_CANNON_SCREEN_THRESHOLD )
+ {
+ StatusEffect_AddTimed( target, eStatusEffect.emp, empViewStrength, empDuration, ARC_CANNON_EMP_FADEOUT_DURATION )
+ EmitSoundOnEntityOnlyToPlayer( target, target, ARC_CANNON_PILOT_SCREEN_SFX )
+ }
+ }
+
+ // Do 3rd person effect on the body
+ asset effect
+ string tag
+ target.TakeDamage( damageAmount, zapInfo.player, zapInfo.player, { origin = beamEndPos, force = Vector(0,0,0), scriptType = deathPackage, weapon = zapInfo.weapon, damageSourceId = dmgSourceID,criticalHitScale = critScale } )
+ //vector dir = Normalize( beamEndPos - beamStartPos )
+ //vector velocity = dir * 600
+ //PushPlayerAway( target, velocity )
+ //PushPlayerAway( expect entity( zapInfo.player ), -velocity )
+
+ if ( IsValid( weapon ) && hasFastPacitor )
+ {
+ if ( IsAlive( target ) && IsAlive( expect entity( zapInfo.player ) ) && target.IsTitan() )
+ {
+ float pushPercent = GraphCapped( damageAmount, damageMin, damageMax, 0.0, 1.0 )
+
+ if ( pushPercent > 0.6 )
+ PushPlayersApart( target, expect entity( zapInfo.player ), pushPercent * 400.0 )
+ }
+ }
+
+ if ( zapInfo.chargeFrac < ARC_CANNON_SCREEN_THRESHOLD )
+ empDuration = ARC_CANNON_3RD_PERSON_EFFECT_MIN_DURATION
+ else
+ empDuration += ARC_CANNON_EMP_FADEOUT_DURATION
+
+ if ( target.GetArmorType() == ARMOR_TYPE_HEAVY )
+ {
+ effect = $"impact_arc_cannon_titan"
+ tag = "exp_torso_front"
+ }
+ else
+ {
+ effect = $"P_emp_body_human"
+ tag = "CHESTFOCUS"
+ }
+
+ if ( target.IsPlayer() )
+ {
+ if ( target.LookupAttachment( tag ) != 0 )
+ ClientStylePlayFXOnEntity( effect, target, tag, empDuration )
+ }
+
+ if ( target.IsPlayer() )
+ EmitSoundOnEntityExceptToPlayer( target, target, "Titan_Blue_Electricity_Cloud" )
+ else
+ EmitSoundOnEntity( target, "Titan_Blue_Electricity_Cloud" )
+
+ thread FadeOutSoundOnEntityAfterDelay( target, "Titan_Blue_Electricity_Cloud", empDuration * 0.6666, empDuration * 0.3333 )
+ }
+ else
+ {
+ //Don't bounce if the beam is set to do 0 damage.
+ chainNum = zapInfo.maxChains
+ }
+
+ if ( isMissile )
+ {
+ if ( IsValid( zapInfo.player ) )
+ target.SetOwner( zapInfo.player )
+ target.MissileExplode()
+ }
+ }
+ #endif // SERVER
+}
+
+
+#if SERVER
+
+void function PushEntForTime( entity ent, vector velocity, float time )
+{
+ ent.EndSignal( "OnDeath" )
+ float endTime = Time() + time
+ float startTime = Time()
+ for ( ;; )
+ {
+ if ( Time() >= endTime )
+ break
+ float multiplier = Graph( Time(), startTime, endTime, 1.0, 0.0 )
+ vector currentVel = ent.GetVelocity()
+ currentVel += velocity * multiplier
+ ent.SetVelocity( currentVel )
+ WaitFrame()
+ }
+}
+
+array<entity> function GetArcCannonChainTargets( vector fromOrigin, entity fromTarget, table zapInfo )
+{
+ // NOTE: fromTarget could be null/invalid if it was a drone
+ array<entity> results = []
+ if ( !IsValid( zapInfo.player ) )
+ return results
+
+ int playerTeam = expect entity( zapInfo.player ).GetTeam()
+ array<entity> allTargets = GetArcCannonTargetsInRange( fromOrigin, playerTeam, expect entity( zapInfo.weapon ) )
+ allTargets = ArrayClosest( allTargets, fromOrigin )
+
+ local viewVector
+ if ( zapInfo.player.IsPlayer() )
+ viewVector = zapInfo.player.GetViewVector()
+ else
+ viewVector = AnglesToForward( zapInfo.player.EyeAngles() )
+
+ local eyePosition = zapInfo.player.EyePosition()
+
+ foreach ( ent in allTargets )
+ {
+ local forkCount = ARC_CANNON_FORK_COUNT_MAX
+ if ( zapInfo.weapon.HasMod( "splitter" ) )
+ forkCount = SPLITTER_FORK_COUNT_MAX
+ else if ( zapInfo.weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ forkCount = ARC_CANNON_FORK_COUNT_MAX_BURN
+
+ if ( results.len() >= forkCount )
+ break
+
+ if ( ent.IsPhaseShifted() )
+ continue
+
+ if ( ent.IsPlayer() )
+ {
+ // Ignore players that are passing damage to their parent. This is to address zapping a friendly rodeo player
+ local entParent = ent.GetParent()
+ if ( IsValid( entParent ) && ent.kv.PassDamageToParent.tointeger() )
+ continue
+
+ // only chains to other titan players for now
+ if ( !ent.IsTitan() )
+ continue
+ }
+
+ if ( ent.GetClassName() == "script_mover" )
+ continue
+
+ if ( IsEntANeutralMegaTurret( ent, playerTeam ) )
+ continue
+
+ if ( !IsAlive( ent ) )
+ continue
+
+ // Don't consider targets that already got zapped
+ if ( ent in zapInfo.zappedTargets )
+ continue
+
+ //Preventing the arc-cannon from firing behind.
+ local vecToEnt = ( ent.GetWorldSpaceCenter() - eyePosition )
+ vecToEnt.Norm()
+ local dotVal = DotProduct( vecToEnt, viewVector )
+ if ( dotVal < 0 )
+ continue
+
+ // Check if we can see them, they aren't behind a wall or something
+ local ignoreEnts = []
+ ignoreEnts.append( zapInfo.player )
+ ignoreEnts.append( ent )
+
+ foreach( zappedTarget, val in zapInfo.zappedTargets )
+ {
+ if ( IsValid( zappedTarget ) )
+ ignoreEnts.append( zappedTarget )
+ }
+
+ TraceResults traceResult = TraceLineHighDetail( fromOrigin, ent.GetWorldSpaceCenter(), ignoreEnts, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+
+ // Trace failed, lets try an eye to eye trace
+ if ( traceResult.fraction < 1 )
+ {
+ // 'fromTarget' may be invalid
+ if ( IsValid( fromTarget ) )
+ traceResult = TraceLineHighDetail( fromTarget.EyePosition(), ent.EyePosition(), ignoreEnts, (TRACE_MASK_PLAYERSOLID_BRUSHONLY | TRACE_MASK_BLOCKLOS), TRACE_COLLISION_GROUP_NONE )
+ }
+
+ if ( traceResult.fraction < 1 )
+ continue
+
+ // Enemy is in visible, and within range.
+ if ( !results.contains( ent ) )
+ results.append( ent )
+ }
+
+ //printt( "NEARBY TARGETS VALID AND VISIBLE:", results.len() )
+
+ return results
+}
+#endif // SERVER
+
+bool function IsEntANeutralMegaTurret( ent, int playerTeam )
+{
+ expect entity( ent )
+
+ if ( ent.GetClassName() != "npc_turret_mega" )
+ return false
+ int entTeam = ent.GetTeam()
+ if ( entTeam == playerTeam )
+ return false
+ if ( !IsEnemyTeam( playerTeam, entTeam ) )
+ return true
+
+ return false
+}
+
+function ArcCannon_HideIdleEffect( entity weapon, delay )
+{
+ bool weaponOwnerIsPilot = IsPilot( weapon.GetWeaponOwner() )
+ weapon.EndSignal( ARC_CANNON_SIGNAL_DEACTIVATED )
+ if ( weaponOwnerIsPilot == false )
+ {
+ weapon.StopWeaponEffect( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity" )
+ weapon.StopWeaponSound( "arc_cannon_charged_loop" )
+ }
+ wait delay
+
+ if ( !IsValid( weapon ) )
+ return
+
+ entity weaponOwner = weapon.GetWeaponOwner()
+ //The weapon can be valid, but the player isn't a Titan during melee execute.
+ // JFS: threads with waits should just end on "OnDestroy"
+ if ( !IsValid( weaponOwner ) )
+ return
+
+ if ( weapon != weaponOwner.GetActiveWeapon() )
+ return
+
+ if ( weaponOwnerIsPilot == false )
+ {
+ weapon.PlayWeaponEffectNoCull( $"wpn_arc_cannon_electricity_fp", $"wpn_arc_cannon_electricity", "muzzle_flash" )
+ weapon.EmitWeaponSound( "arc_cannon_charged_loop" )
+ }
+ else
+ {
+ weapon.EmitWeaponSound_1p3p( "Arc_Rifle_charged_Loop_1P", "Arc_Rifle_charged_Loop_3P" )
+ }
+}
+
+#if SERVER
+void function AddToArcCannonTargets( entity ent )
+{
+ AddToScriptManagedEntArray( level._arcCannonTargetsArrayID, ent );
+}
+
+function RemoveArcCannonTarget( ent )
+{
+ RemoveFromScriptManagedEntArray( level._arcCannonTargetsArrayID, ent )
+}
+
+array<entity> function GetArcCannonTargets( vector origin, int team, entity weapon )
+{
+ array<entity> targets = GetScriptManagedEntArrayWithinCenter( level._arcCannonTargetsArrayID, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN )
+
+ if ( ARC_CANNON_TARGETS_MISSILES && weapon.GetWeaponChargeFraction() == 1.0 )
+ targets.extend( GetProjectileArrayEx( "rpg_missile", TEAM_ANY, team, origin, ARC_CANNON_TITAN_RANGE_CHAIN ) )
+
+ return targets
+}
+
+array<entity> function GetArcCannonTargetsInRange( vector origin, int team, entity weapon )
+{
+ array<entity> allTargets = GetArcCannonTargets( origin, team, weapon )
+ array<entity> targetsInRange
+
+ float titanDistSq
+ float distSq
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ {
+ titanDistSq = ARC_CANNON_TITAN_RANGE_CHAIN_BURN * ARC_CANNON_TITAN_RANGE_CHAIN_BURN
+ distSq = ARC_CANNON_RANGE_CHAIN_BURN * ARC_CANNON_RANGE_CHAIN_BURN
+ }
+ else
+ {
+ titanDistSq = ARC_CANNON_TITAN_RANGE_CHAIN * ARC_CANNON_TITAN_RANGE_CHAIN
+ distSq = ARC_CANNON_RANGE_CHAIN * ARC_CANNON_RANGE_CHAIN
+ }
+
+ foreach( target in allTargets )
+ {
+ float d = DistanceSqr( target.GetOrigin(), origin )
+ float validDist = target.IsTitan() ? titanDistSq : distSq
+ if ( d <= validDist )
+ targetsInRange.append( target )
+ }
+
+ return targetsInRange
+}
+#endif // SERVER
+
+function CreateArcCannonBeam( weapon, target, startPos, endPos, player, lifeDuration = ARC_CANNON_BEAM_LIFETIME, radius = 256, boltWidth = 4, noiseAmplitude = 5, hasTarget = true, firstBeam = false )
+{
+ Assert( startPos )
+ Assert( endPos )
+
+ //**************************
+ // LIGHTNING BEAM EFFECT
+ //**************************
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ lifeDuration = ARC_CANNON_BEAM_LIFETIME_BURN
+ // If it's the first beam and on client we do a special beam so it's lined up with the muzzle origin
+ #if CLIENT
+ if ( firstBeam )
+ thread CreateClientArcBeam( weapon, endPos, lifeDuration, target )
+ #endif
+
+ #if SERVER
+ // Control point sets the end position of the effect
+ entity cpEnd = CreateEntity( "info_placement_helper" )
+ SetTargetName( cpEnd, UniqueString( "arc_cannon_beam_cpEnd" ) )
+ cpEnd.SetOrigin( endPos )
+ DispatchSpawn( cpEnd )
+
+ entity zapBeam = CreateEntity( "info_particle_system" )
+ zapBeam.kv.cpoint1 = cpEnd.GetTargetName()
+
+ zapBeam.SetValueForEffectNameKey( GetBeamEffect( weapon ) )
+
+ zapBeam.kv.start_active = 0
+ zapBeam.SetOwner( player )
+ zapBeam.SetOrigin( startPos )
+ if ( firstBeam )
+ {
+ zapBeam.kv.VisibilityFlags = (ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_ENEMY) // everyone but owner
+ zapBeam.SetParent( player.GetActiveWeapon(), "muzzle_flash", false, 0.0 )
+ }
+ DispatchSpawn( zapBeam )
+
+ zapBeam.Fire( "Start" )
+ zapBeam.Fire( "StopPlayEndCap", "", lifeDuration )
+ zapBeam.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ cpEnd.Kill_Deprecated_UseDestroyInstead( lifeDuration )
+ #endif
+}
+
+function GetBeamEffect( weapon )
+{
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ return ARC_CANNON_BEAM_EFFECT_MOD
+
+ return ARC_CANNON_BEAM_EFFECT
+}
+
+#if CLIENT
+function CreateClientArcBeam( weapon, endPos, lifeDuration, target )
+{
+ Assert( IsClient() )
+
+ local beamEffect = GetBeamEffect( weapon )
+
+ // HACK HACK HACK HACK
+ string tag = "muzzle_flash"
+ if ( weapon.GetWeaponInfoFileKeyField( "client_tag_override" ) != null )
+ tag = expect string( weapon.GetWeaponInfoFileKeyField( "client_tag_override" ) )
+
+ local handle = weapon.PlayWeaponEffectReturnViewEffectHandle( beamEffect, $"", tag )
+ if ( !EffectDoesExist( handle ) )
+ return
+
+ EffectSetControlPointVector( handle, 1, endPos )
+
+ if ( weapon.HasMod( "burn_mod_titan_arc_cannon" ) )
+ lifeDuration = ARC_CANNON_BEAM_LIFETIME_BURN
+
+ wait( lifeDuration )
+
+ if ( IsValid( weapon ) )
+ weapon.StopWeaponEffect( beamEffect, $"" )
+}
+
+void function ClientDestroyCallback_ArcCannon_Stop( entity ent )
+{
+ ArcCannon_Stop( ent )
+}
+#endif // CLIENT
+
+function GetArcCannonChargeFraction( weapon )
+{
+ if ( IsValid( weapon ) )
+ {
+ local chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO
+ if ( weapon.HasMod( "capacitor" ) )
+ chargeRatio = ARC_CANNON_CAPACITOR_CHARGE_RATIO
+ if ( weapon.GetWeaponSettingBool( eWeaponVar.is_burn_mod ) )
+ chargeRatio = ARC_CANNON_DAMAGE_CHARGE_RATIO_BURN
+ return chargeRatio
+ }
+
+ return 0
+}
+
+function GetWeaponChargeFrac( weapon )
+{
+ if ( weapon.IsChargeWeapon() )
+ return weapon.GetWeaponChargeFraction()
+ return 1.0
+}
diff --git a/Northstar.Custom/mod/scripts/vscripts/weapons/mp_titanweapon_arc_cannon.nut b/Northstar.Custom/mod/scripts/vscripts/weapons/mp_titanweapon_arc_cannon.nut
new file mode 100644
index 00000000..78879393
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/weapons/mp_titanweapon_arc_cannon.nut
@@ -0,0 +1,226 @@
+untyped
+
+global function MpTitanweaponArcCannon_Init
+
+global function OnWeaponActivate_titanweapon_arc_cannon
+global function OnWeaponDeactivate_titanweapon_arc_cannon
+global function OnWeaponReload_titanweapon_arc_cannon
+global function OnWeaponOwnerChanged_titanweapon_arc_cannon
+global function OnWeaponChargeBegin_titanweapon_arc_cannon
+global function OnWeaponChargeEnd_titanweapon_arc_cannon
+global function OnWeaponPrimaryAttack_titanweapon_arc_cannon
+
+const FX_EMP_BODY_HUMAN = $"P_emp_body_human"
+const FX_EMP_BODY_TITAN = $"P_emp_body_titan"
+
+#if SERVER
+global function OnWeaponNpcPrimaryAttack_titanweapon_arc_cannon
+#endif // #if SERVER
+
+void function MpTitanweaponArcCannon_Init()
+{
+ ArcCannon_PrecacheFX()
+
+ #if SERVER
+ AddDamageCallbackSourceID( eDamageSourceId.mp_titanweapon_arc_cannon, ArcRifleOnDamage )
+ #endif
+}
+
+void function OnWeaponActivate_titanweapon_arc_cannon( entity weapon )
+{
+ entity weaponOwner = weapon.GetWeaponOwner()
+ thread DelayedArcCannonStart( weapon, weaponOwner )
+ if( !("weaponOwner" in weapon.s) )
+ weapon.s.weaponOwner <- weaponOwner
+}
+
+function DelayedArcCannonStart( entity weapon, entity weaponOwner )
+{
+ weapon.EndSignal( "WeaponDeactivateEvent" )
+
+ WaitFrame()
+
+ if ( IsValid( weapon ) && IsValid( weaponOwner ) && weapon == weaponOwner.GetActiveWeapon() )
+ {
+ if( weaponOwner.IsPlayer() )
+ {
+ entity modelEnt = weaponOwner.GetViewModelEntity()
+ if( IsValid( modelEnt ) && EntHasModelSet( modelEnt ) )
+ ArcCannon_Start( weapon )
+ }
+ else
+ {
+ ArcCannon_Start( weapon )
+ }
+ }
+}
+
+void function OnWeaponDeactivate_titanweapon_arc_cannon( entity weapon )
+{
+ ArcCannon_ChargeEnd( weapon, expect entity( weapon.s.weaponOwner ) )
+ ArcCannon_Stop( weapon )
+}
+
+void function OnWeaponReload_titanweapon_arc_cannon( entity weapon, int milestoneIndex )
+{
+ local reloadTime = weapon.GetWeaponInfoFileKeyField( "reload_time" )
+ thread ArcCannon_HideIdleEffect( weapon, reloadTime ) //constant seems to help it sync up better
+}
+
+void function OnWeaponOwnerChanged_titanweapon_arc_cannon( entity weapon, WeaponOwnerChangedParams changeParams )
+{
+ #if CLIENT
+ entity viewPlayer = GetLocalViewPlayer()
+ if ( changeParams.oldOwner != null && changeParams.oldOwner == viewPlayer )
+ {
+ ArcCannon_ChargeEnd( weapon, changeParams.oldOwner )
+ ArcCannon_Stop( weapon)
+ }
+
+ if ( changeParams.newOwner != null && changeParams.newOwner == viewPlayer )
+ thread ArcCannon_HideIdleEffect( weapon, 0.25 )
+ #else
+ if ( changeParams.oldOwner != null )
+ {
+ ArcCannon_ChargeEnd( weapon, changeParams.oldOwner )
+ ArcCannon_Stop( weapon )
+ }
+
+ if ( changeParams.newOwner != null )
+ thread ArcCannon_HideIdleEffect( weapon, 0.25 )
+ #endif
+}
+
+bool function OnWeaponChargeBegin_titanweapon_arc_cannon( entity weapon )
+{
+ local stub = "this is here to suppress the untyped message. This can go away when the .s. usage is removed from this file."
+ #if SERVER
+ //if ( weapon.HasMod( "fastpacitor_push_apart" ) )
+ // weapon.GetWeaponOwner().StunMovementBegin( weapon.GetWeaponSettingFloat( eWeaponVar.charge_time ) )
+ #endif
+
+ ArcCannon_ChargeBegin( weapon )
+
+ return true
+}
+
+void function OnWeaponChargeEnd_titanweapon_arc_cannon( entity weapon )
+{
+ ArcCannon_ChargeEnd( weapon, weapon )
+}
+
+var function OnWeaponPrimaryAttack_titanweapon_arc_cannon( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ if ( weapon.HasMod( "capacitor" ) && weapon.GetWeaponChargeFraction() < GetArcCannonChargeFraction( weapon ) )
+ return 0
+
+ if ( !attackParams.firstTimePredicted )
+ return
+
+ local fireRate = weapon.GetWeaponInfoFileKeyField( "fire_rate" )
+ thread ArcCannon_HideIdleEffect( weapon, (1 / fireRate) )
+
+ return FireArcCannon( weapon, attackParams )
+}
+
+#if SERVER
+var function OnWeaponNpcPrimaryAttack_titanweapon_arc_cannon( entity weapon, WeaponPrimaryAttackParams attackParams )
+{
+ local fireRate = weapon.GetWeaponInfoFileKeyField( "fire_rate" )
+ thread ArcCannon_HideIdleEffect( weapon, fireRate )
+
+ return FireArcCannon( weapon, attackParams )
+}
+
+void function ArcRifleOnDamage( entity ent, var damageInfo )
+{
+ vector pos = DamageInfo_GetDamagePosition( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+
+ EmitSoundOnEntity( ent, ARC_CANNON_TITAN_SCREEN_SFX )
+
+ if ( ent.IsPlayer() || ent.IsNPC() )
+ {
+ entity entToSlow = ent
+ entity soul = ent.GetTitanSoul()
+
+ if ( soul != null )
+ entToSlow = soul
+
+ StatusEffect_AddTimed( entToSlow, eStatusEffect.move_slow, 0.5, 2.0, 1.0 )
+ StatusEffect_AddTimed( entToSlow, eStatusEffect.dodge_speed_slow, 0.5, 2.0, 1.0 )
+ }
+
+ string tag = ""
+ asset effect
+
+ if ( ent.IsTitan() )
+ {
+ tag = "exp_torso_front"
+ effect = FX_EMP_BODY_TITAN
+ }
+ else if ( ChestFocusTarget( ent ) )
+ {
+ tag = "CHESTFOCUS"
+ effect = FX_EMP_BODY_HUMAN
+ }
+ else if ( IsAirDrone( ent ) )
+ {
+ tag = "HEADSHOT"
+ effect = FX_EMP_BODY_HUMAN
+ }
+ else if ( IsGunship( ent ) )
+ {
+ tag = "ORIGIN"
+ effect = FX_EMP_BODY_TITAN
+ }
+
+ if ( tag != "" )
+ {
+ float duration = 2.0
+ //thread EMP_FX( effect, ent, tag, duration )
+ }
+
+ if ( ent.IsTitan() )
+ {
+ if ( ent.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, "titan_energy_bulletimpact_3p_vs_1p" )
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "titan_energy_bulletimpact_3p_vs_3p" )
+ }
+ else
+ {
+ EmitSoundOnEntity( ent, "titan_energy_bulletimpact_3p_vs_3p" )
+ }
+ }
+ else
+ {
+ if ( ent.IsPlayer() )
+ {
+ EmitSoundOnEntityOnlyToPlayer( ent, ent, "flesh_lavafog_deathzap_3p" )
+ EmitSoundOnEntityExceptToPlayer( ent, ent, "flesh_lavafog_deathzap_1p" )
+ }
+ else
+ {
+ EmitSoundOnEntity( ent, "flesh_lavafog_deathzap_1p" )
+ }
+ }
+
+}
+
+bool function ChestFocusTarget( entity ent )
+{
+ if ( IsSpectre( ent ) )
+ return true
+ if ( IsStalker( ent ) )
+ return true
+ if ( IsSuperSpectre( ent ) )
+ return true
+ if ( IsGrunt( ent ) )
+ return true
+ if ( IsPilot( ent ) )
+ return true
+
+ return false
+}
+#endif // #if SERVER
diff --git a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt
new file mode 100644
index 00000000..2672dca9
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_arc_cannon.txt
@@ -0,0 +1,349 @@
+WeaponData
+{
+ // General
+ "printname" "#WPN_TITAN_ARC_CANNON"
+ "shortprintname" "#WPN_TITAN_ARC_CANNON_SHORT"
+ "description" "#WPN_TITAN_ARC_CANNON_DESC"
+ "longdesc" "#WPN_TITAN_ARC_CANNON_LONGDESC"
+ "weaponClass" "titan"
+ "fire_mode" "semi-auto"
+ "pickup_hold_prompt" "Hold [USE] [WEAPONNAME]"
+ "pickup_press_prompt" "[USE] [WEAPONNAME]"
+ "minimap_reveal_distance" "32000"
+
+ // Menu Stats
+ "stat_damage" "85"
+ "stat_range" "35"
+ "stat_accuracy" "80"
+ "stat_rof" "20"
+
+ // Models
+ "viewmodel" "models/weapons/titan_arc_rifle/atpov_titan_arc_rifle.mdl"
+ "playermodel" "models/weapons/titan_arc_rifle/w_titan_arc_rifle.mdl"
+ "anim_prefix" "ar2"
+
+
+ "OnWeaponActivate" "OnWeaponActivate_titanweapon_arc_cannon"
+ "OnWeaponDeactivate" "OnWeaponDeactivate_titanweapon_arc_cannon"
+ "OnWeaponReload" "OnWeaponReload_titanweapon_arc_cannon"
+ "OnWeaponOwnerChanged" "OnWeaponOwnerChanged_titanweapon_arc_cannon"
+ "OnWeaponChargeBegin" "OnWeaponChargeBegin_titanweapon_arc_cannon"
+ "OnWeaponChargeEnd" "OnWeaponChargeEnd_titanweapon_arc_cannon"
+ "OnWeaponPrimaryAttack" "OnWeaponPrimaryAttack_titanweapon_arc_cannon"
+ "OnWeaponNpcPrimaryAttack" "OnWeaponNpcPrimaryAttack_titanweapon_arc_cannon"
+// "OnWeaponCooldown" "OnWeaponCooldown_titanweapon_particle_accelerator"
+
+
+
+ // Effects
+ //"tracer_effect" "weapon_tracers_xo16"
+ //Impact Table used for visuals at the top of arc_cannon.nut
+ "tracer_effect" "P_wpn_arcball_beam"
+ "tracer_effect_first_person" "P_wpn_arcball_beam"
+ "impact_effect_table" "exp_arc_cannon"
+ "adjust_to_gun_barrel" "1"
+ "fx_muzzle_flash_view" "wpn_arc_cannon_electricity_fp"
+ "fx_muzzle_flash_world" "wpn_arc_cannon_electricity"
+ "fx_muzzle_flash_attach" "muzzle_flash"
+
+ // Damage - When Used by Players
+ "damage_type" "bullet"
+ "damage_near_distance" "200"
+ "damage_far_distance" "2500"
+ "damage_near_value" "220"
+ "damage_far_value" "170"
+ "damage_near_value_titanarmor" "1800"
+ "damage_far_value_titanarmor" "100"
+
+ // Damage - When Used by NPCs
+ "npc_damage_near_distance" "200"
+ "npc_damage_far_distance" "2500"
+ "npc_damage_near_value" "220"
+ "npc_damage_far_value" "170"
+ "npc_damage_near_value_titanarmor" "1800"
+ "npc_damage_far_value_titanarmor" "100"
+
+ "critical_hit" "0"
+ "critical_hit_damage_scale" "1.5"
+
+ // Ammo
+ "ammo_min_to_fire" "1"
+ "ammo_no_remove_from_stockpile" "1"
+
+ // Behavior
+ "fire_rate" "1"
+// "rechamber_time" "0.25" //"1.30"
+ "cooldown_time" "0.6"
+ "reloadempty_time" "6.03"
+ "reloadempty_time_late1" "4.7"
+ "reloadempty_time_late2" "3.5"
+ "reloadempty_time_late3" "2.5"
+ "reloadempty_time_late4" "1.43"
+ "reloadempty_time_late5" "0.56"
+ "zoom_time_in" "0.1"
+ "zoom_time_out" "0.1"
+ "zoom_fov" "33"
+ "reload_time" "3.5"
+ "reloadempty_time" "3.5"
+ "holster_time" ".45"
+ "deploy_time" ".85"
+ "lower_time" ".1"
+ "raise_time" ".4"
+ "charge_time" "3.7"
+ "charge_cooldown_time" "1.0"
+ "charge_end_forces_fire" "0"
+ "allow_empty_fire" "1"
+ "reload_enabled" "0"
+ "allow_empty_click" "1"
+ "empty_reload_only" "0"
+ "trigger_snipercam" "1"
+ "allow_headshots" "0"
+ "bypass_semiauto_hold_protection" "1"
+ "vortex_drain" ".15"
+ "charge_effect_1p" "wpn_arc_cannon_charge_fp"
+ "charge_effect_3p" "wpn_arc_cannon_charge"
+ "charge_effect_attachment" "muzzle_flash"
+
+
+
+ // Spread
+ "spread_stand_hip" "10"
+ "spread_npc" "2"
+
+ "ammo_suck_behavior" "primary_weapons"
+
+ // View Kick
+ "viewkick_spring" "titan_arc"
+
+ "viewkick_pitch_base" "-1"
+ "viewkick_pitch_random" "0.5"
+ "viewkick_pitch_softScale" "1"
+ "viewkick_pitch_hardScale" "0"
+
+ "viewkick_yaw_base" "0"
+ "viewkick_yaw_random" "0.5"
+ "viewkick_yaw_softScale" "1"
+ "viewkick_yaw_hardScale" "0"
+
+ "viewkick_roll_base" "0.0"
+ "viewkick_roll_randomMin" "0.3"
+ "viewkick_roll_randomMax" "0.45"
+ "viewkick_roll_softScale" "0.2"
+ "viewkick_roll_hardScale" "1.5"
+
+ "viewkick_hipfire_weaponFraction" "0.5"
+ "viewkick_hipfire_weaponFraction_vmScale" "0.75"
+ "viewkick_ads_weaponFraction" "0.6"
+ "viewkick_ads_weaponFraction_vmScale" "0.2"
+
+
+ // Bob
+ "bob_cycle_time" "0.7"
+ "bob_vert_dist" "0.5"
+ "bob_horz_dist" "1"
+ "bob_max_speed" "150"
+ "bob_pitch" "1"
+ "bob_yaw" "1"
+ "bob_roll" "-0.75"
+
+ // View Drift
+
+ // Rumble
+ "fire_rumble" "titan_arc_cannon"
+
+ // Sway
+ "sway_rotate_attach" "SWAY_ROTATE"
+ "sway_min_x" "-0.3"
+ "sway_min_y" "-0.5"
+ "sway_min_z" "-0.5"
+ "sway_max_x" "0.3"
+ "sway_max_y" "0.5"
+ "sway_max_z" "0.1"
+ "sway_min_pitch" "-3"
+ "sway_min_yaw" "-3.5"
+ "sway_min_roll" "-1"
+ "sway_max_pitch" "3"
+ "sway_max_yaw" "3.5"
+ "sway_max_roll" "2"
+ "sway_translate_gain" "10"
+ "sway_rotate_gain" "12"
+ "sway_move_forward_translate_x" "0"
+ "sway_move_forward_translate_z" "-0.5"
+ "sway_move_back_translate_x" "-2"
+ "sway_move_back_translate_z" "-1"
+ "sway_move_left_translate_y" "-1"
+ "sway_move_left_translate_z" "-0.5"
+ "sway_move_left_rotate_roll" "-1"
+ "sway_move_right_translate_y" "1"
+ "sway_move_right_translate_z" "-0.5"
+ "sway_move_right_rotate_roll" "2"
+ "sway_move_up_translate_z" "-1"
+ "sway_move_down_translate_z" "1"
+ "sway_turn_left_rotate_yaw" "-1"
+ "sway_turn_right_rotate_yaw" "1"
+ "sway_turn_up_rotate_pitch" "1"
+ "sway_turn_down_rotate_pitch" "-1"
+
+ // NPC
+ "proficiency_poor_spreadscale" "5.0"
+ "proficiency_poor_bias" "1.0"
+ "proficiency_average_spreadscale" "4.0"
+ "proficiency_average_bias" "1.0"
+ "proficiency_good_spreadscale" "3.0"
+ "proficiency_good_bias" "1.0"
+ "proficiency_very_good_spreadscale" "2.3"
+ "proficiency_very_good_bias" "1.0"
+ "proficiency_perfect_spreadscale" "1.7"
+ "proficiency_perfect_bias" "1.0"
+
+ "npc_min_range" "0"
+ "npc_max_range" "2500"
+ "npc_min_range_secondary" "0"
+ "npc_max_range_secondary" "2500"
+ "npc_min_burst" "1"
+ "npc_max_burst" "1"
+ "rest_time_between_bursts_min" "2.5"
+ "rest_time_between_bursts_max" "3.0"
+
+ // WeaponED Unhandled Key/Values and custom script Key/Values
+ "sound_dryfire" "titan_dryfire"
+ "viewdrift_hipfire_stand_scale_pitch" "0.1"
+ "viewdrift_hipfire_crouch_scale_pitch" "0.1"
+ "viewdrift_hipfire_air_scale_pitch" "0.1"
+ "viewdrift_hipfire_stand_scale_yaw" "0.075"
+ "viewdrift_hipfire_crouch_scale_yaw" "0.075"
+ "viewdrift_hipfire_air_scale_yaw" "0.075"
+ "viewdrift_hipfire_speed_pitch" "0.6"
+ "viewdrift_hipfire_speed_yaw" "1.22"
+ "viewdrift_ads_stand_scale_pitch" "0.05"
+ "viewdrift_ads_crouch_scale_pitch" "0.05"
+ "viewdrift_ads_air_scale_pitch" "0.05"
+ "viewdrift_ads_stand_scale_yaw" "0.037"
+ "viewdrift_ads_crouch_scale_yaw" "0.037"
+ "viewdrift_ads_air_scale_yaw" "0.037"
+ "viewdrift_ads_speed_pitch" "0.6"
+ "viewdrift_ads_speed_yaw" "1.22"
+ "npc_reload_enabled" "0"
+ "npc_vortex_block" "1"
+
+ // Crosshair
+ "red_crosshair_range" "2500"
+
+ Mods
+ {
+ overcharge
+ {
+ //overcharge
+ }
+ capacitor
+ {
+ "charge_time" "2.5" //for reference was 3 in 10/15 evening playtest
+ "charge_cooldown_time" "1.0"
+ "charge_cooldown_delay" "0.0"
+ //"crosshair_index" "1"
+ "spread_stand_hip" "15"
+ "damage_far_distance" "2700"
+ "damage_near_value_titanarmor" "2000"
+ }
+ splitter
+ {
+ "damage_near_value_titanarmor" "1900"
+ "damage_far_value_titanarmor" "100"
+ }
+ burn_mod_titan_arc_cannon
+ {
+ //"crosshair_index" "2"
+ "tracer_effect" "wpn_arc_cannon_beam_mod"
+ "tracer_effect_first_person" "wpn_arc_cannon_beam_mod"
+ "damage_near_value" "*1.1"
+ "damage_far_value" "*1.1"
+ "damage_near_value_titanarmor" "*1.1"
+ "damage_far_value_titanarmor" "*1.1"
+ "is_burn_mod" "1"
+ }
+ }
+
+ active_crosshair_count "2"
+// rui_crosshair_index "1"
+ "ui1_enable" "1"
+ "ui1_draw_cloaked" "0"
+ UiData1
+ {
+ "ui" "ui/crosshair_charge_rifle"
+ "mesh" "models/weapons/attachments/alternator_rui_upper"
+ Args
+ {
+ adjustedSpread weapon_spread
+ adsFrac player_zoomFrac
+ isSprinting player_is_sprinting
+ isReloading weapon_is_reloading
+ readyFrac progress_ready_to_fire_frac
+ teamColor crosshair_team_color
+ isAmped weapon_is_amped
+ chargeFrac player_chargeFrac
+ crosshairMovementX crosshair_movement_x
+ crosshairMovementY crosshair_movement_y
+ }
+ }
+ RUI_CrosshairData
+ {
+ DefaultArgs
+ {
+ adjustedSpread weapon_spread
+ adsFrac player_zoomFrac
+ isSprinting player_is_sprinting
+ isReloading weapon_is_reloading
+ readyFrac progress_ready_to_fire_frac
+ teamColor crosshair_team_color
+ isAmped weapon_is_amped
+ chargeFrac player_chargeFrac
+ crosshairMovementX crosshair_movement_x
+ crosshairMovementY crosshair_movement_y
+ }
+
+ Crosshair_1
+ {
+ "ui" "ui/crosshair_charge_rifle"
+// "ui" "ui/alternator_reticle"
+ "base_spread" "10.0"
+ Args
+ {
+ isFiring weapon_is_firing
+ }
+ Element0
+ {
+ "fade_while_sprinting" "1"
+ "fade_while_reloading" "1"
+ "stationary" "1"
+ "default_color" "246 134 40 255"
+ "type" "static"
+ "material" $"vgui/hud/arc_cannon_charge/arc_cannon_charge"
+ "size_x" "80"
+ "size_y" "80"
+ "scale_ads" "1.5"
+ }
+ Element1
+ {
+ "fade_while_sprinting" "1"
+ "fade_while_reloading" "1"
+ "stationary" "1"
+ "default_color" "246 134 40 255"
+ "type" "static"
+ "material" "vgui/hud/arc_cannon_charge/arc_cannon_shadow_horizontal"
+ "size_x" "80"
+ "size_y" "80"
+ "scale_ads" "1.5"
+ }
+ }
+
+ Crosshair_2
+ {
+ "ui" "ui/crosshair_circle2"
+ "base_spread" "0.0"
+ Args
+ {
+ isFiring weapon_is_firing
+ }
+ }
+ }
+}
diff --git a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt
index a1337d9f..09aac6ea 100644
--- a/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt
+++ b/Northstar.Custom/mod/scripts/weapons/mp_titanweapon_triplethreat.txt
@@ -102,6 +102,8 @@ WeaponData
// Damage - When Used by Players
"damage_type" "burn"
+ "show_grenade_indicator" "1"
+
"crosshair" "crosshair_t"
"explosion_damage" "350" // 150
"explosion_damage_heavy_armor" "550" // 150
diff --git a/Northstar.Custom/mod/scripts/weapons/mp_weapon_shotgun_doublebarrel.txt b/Northstar.Custom/mod/scripts/weapons/mp_weapon_shotgun_doublebarrel.txt
new file mode 100644
index 00000000..2bc8b094
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/weapons/mp_weapon_shotgun_doublebarrel.txt
@@ -0,0 +1,405 @@
+WeaponData
+{
+ // General
+ "printname" "#WPN_SHOTGUN_DBLBARREL"
+ "shortprintname" "#WPN_SHOTGUN_DBLBARREL_SHORT"
+ "description" "#WPN_SHOTGUN_DBLBARREL_DESC"
+ "longdesc" "#WPN_SHOTGUN_DBLBARREL_LONGDESC"
+ "menu_icon" "rui/weapon_icons/mp_weapon_shotgun_doublebarrel"
+ "hud_icon" "rui/weapon_icons/mp_weapon_shotgun_doublebarrel"
+ "viewmodel_offset_hip" "2 -2 -2"
+ "weaponClass" "human"
+ "weaponSubClass" "shotgun"
+ "body_type" "light"
+ "fire_mode" "semi-auto"
+ "pickup_hold_prompt" "Hold [USE] [WEAPONNAME]"
+ "pickup_press_prompt" "[USE] [WEAPONNAME]"
+ "aimassist_adspull_weaponclass" "broad"
+ "minimap_reveal_distance" "32000"
+ "leveled_pickup" "0"
+
+ // Models
+ "viewmodel" "models/weapons/shotgun_doublebarrel/ptpov_shotgun_doublebarrel.mdl"
+ "playermodel" "models/weapons/shotgun_doublebarrel/w_shotgun_doublebarrel.mdl"
+
+ "OnWeaponPrimaryAttack" "OnWeaponPrimaryAttack_weapon_shotgun"
+ "OnWeaponNpcPrimaryAttack" "OnWeaponNpcPrimaryAttack_weapon_shotgun"
+
+
+ "viewmodel_offset_ads" "0 0 0"
+ "dof_zoom_nearDepthStart" "2.000"
+ "dof_zoom_nearDepthEnd" "4.750"
+ "dof_nearDepthStart" "3.683"
+ "dof_nearDepthEnd" "5.300"
+
+ // Menu
+ "menu_category" "shotgun"
+ "menu_anim_class" "large"
+ "stat_damage" "50"
+ "stat_range" "70"
+ "stat_accuracy" "65"
+ "stat_rof" "30"
+
+ "impulse_force" "800"
+
+ "impact_effect_table" "inc_bullet"
+
+ // Spread
+ "spread_stand_hip" "8.5"
+ "spread_stand_hip_run" "8.5"
+ "spread_stand_hip_sprint" "8.5"
+ "spread_crouch_hip" "8.5"
+ "spread_air_hip" "8.5"
+ "spread_stand_ads" "8.5"
+ "spread_crouch_ads" "8.5"
+ "spread_air_ads" "8.5"
+ "spread_wallrunning" "8.5"
+ "spread_wallhanging" "8.5"
+
+ // Damage - When Used by Players
+ "damage_type" "bullet"
+ "damage_near_distance" "300"
+ "damage_far_distance" "700"
+ "damage_near_value" "220"
+ "damage_far_value" "25"
+ "damage_near_value_titanarmor" "130"
+ "damage_far_value_titanarmor" "10"
+
+ "damage_rodeo" "700"
+ "damage_falloff_type" "inverse"
+ "damage_inverse_distance" "130"
+ "damage_falloff_type" "inverse"
+ "damage_inverse_distance" "100"
+ "damage_flags" "DF_SHOTGUN | DF_BULLET | DF_KNOCK_BACK | DF_DISMEMBERMENT"
+
+ "damage_headshot_scale" "1.25"
+
+ "red_crosshair_range" "750"
+
+ // Damage - When Used by NPCs
+ "npc_damage_near_value" "25"
+ "npc_damage_far_value" "13"
+ "npc_damage_near_value_titanarmor" "40"
+ "npc_damage_far_value_titanarmor" "0"
+
+ "enable_highlight_networking_on_creation" "1"
+
+ "damage_heavyarmor_nontitan_scale" "0.35"
+
+
+ // Ammo
+ "ammo_stockpile_max" "40"
+ "ammo_default_total" "40"
+ "ammo_clip_size" "2"
+ "ammo_no_remove_from_stockpile" "1"
+ "ammo_min_to_fire" "1"
+
+ "reload_time" "1.85"
+ "reload_time_late1" "1.15"
+ "reloadempty_time" "1.85"
+ "reloadempty_time_late1" "1.15"
+
+
+ // Effects
+ "tracer_effect" "weapon_tracers_shotgun"
+ "vortex_absorb_effect" "wpn_vortex_projectile_shotgun_FP"
+ "vortex_absorb_effect_third_person" "wpn_vortex_projectile_shotgun"
+ "vortex_absorb_sound" "Vortex_Shield_AbsorbBulletSmall"
+ "vortex_absorb_sound_1p_vs_3p" "Vortex_Shield_AbsorbBulletSmall_1P_VS_3P"
+ "projectile_adjust_to_gun_barrel" "1"
+
+ "sound_dryfire" "shotgun_dryfire"
+ "sound_pickup" "wpn_pickup_Rifle_1P"
+ "fire_sound_1_player_1p" "Weapon_EVA8_AutoFire_1P"
+ "fire_sound_1_player_3p" "Weapon_EVA8_AutoFire_3P"
+ "fire_sound_1_npc" "Weapon_EVA8_AutoFire_NPC"
+ "sound_zoom_in" "Weapon_EVA8_ADS_In"
+ "sound_zoom_out" "Weapon_EVA8_ADS_Out"
+
+ "low_ammo_sound_name_1" "EVA8_LowAmmo_Shot1"
+ "low_ammo_sound_name_2" "EVA8_LowAmmo_Shot2"
+ "low_ammo_sound_name_3" "EVA8_LowAmmo_Shot3"
+
+ "fx_shell_eject_view" "wpn_shelleject_shotshell_FP"
+ "fx_shell_eject_world" "wpn_shelleject_shotshell"
+ "fx_shell_eject_attach" "shell"
+
+ "fx_muzzle_flash_view" "mflash_shotgun_fp_FULL"
+ "fx_muzzle_flash_world" "mflash_shotgun_FULL"
+ "fx_muzzle_flash_attach" "muzzle_flash"
+
+
+
+ "critical_hit_damage_scale" "1"
+ "critical_hit" "1"
+
+ dof_zoom_focusArea_horizontal 0.036
+ dof_zoom_focusArea_top 0.070
+ dof_zoom_focusArea_bottom -0.023
+
+
+ "titanarmor_critical_hit_required" "1"
+
+
+ // Behavior
+ "fire_rate" "2.75"
+ "zoom_time_in" "0.25"
+ "zoom_time_out" "0.2"
+ "zoom_fov" "55"
+ "holster_time" "0.5"
+ "deploy_time" "0.66"
+ "lower_time" "0.25"
+ "raise_time" "0.3"
+ "vortex_refire_behavior" "bullet"
+ "allow_empty_fire" "0"
+ "reload_enabled" "1"
+ "allow_empty_click" "1"
+ "empty_reload_only" "0"
+ "trigger_snipercam" "0"
+ "allow_headshots" "1"
+ "primary_fire_does_not_block_sprint" "0"
+ "ads_move_speed_scale" "0.75"
+ "aimassist_disable_hipfire" "0"
+ "aimassist_disable_ads" "0"
+ "aimassist_disable_hipfire_titansonly" "1"
+ "aimassist_disable_ads_titansonly" "1"
+ "headshot_distance" "500"
+
+
+ "sprint_fractional_anims" "0"
+
+
+
+ "ammo_suck_behavior" "primary_weapons"
+
+ // View Kick
+ "viewkick_spring" "shotgun"
+
+ "viewkick_pitch_base" "-1.75"
+ "viewkick_pitch_random" "0.75"
+ "viewkick_pitch_softScale" "0.3"
+ "viewkick_pitch_hardScale" "1.5"
+
+ "viewkick_yaw_base" "-0.65"
+ "viewkick_yaw_random" "0.38"
+ "viewkick_yaw_softScale" "0.38"
+ "viewkick_yaw_hardScale" "1.5"
+
+ "viewkick_roll_base" "0"
+ "viewkick_roll_randomMin" "0.6"
+ "viewkick_roll_randomMax" "0.8"
+ "viewkick_roll_softScale" "0.2"
+ "viewkick_roll_hardScale" "2.75"
+
+ "viewkick_hipfire_weaponFraction" "0.1"
+ "viewkick_hipfire_weaponFraction_vmScale" "0.0"
+ "viewkick_ads_weaponFraction" "0.35"
+ "viewkick_ads_weaponFraction_vmScale" "0.25"
+
+ "viewkick_perm_pitch_base" "-0.5"
+ "viewkick_perm_pitch_random" "1.1"
+ "viewkick_perm_pitch_random_innerexclude" "0.5"
+ "viewkick_perm_yaw_base" "0.0"
+ "viewkick_perm_yaw_random" "1.5"
+ "viewkick_perm_yaw_random_innerexclude" "0.5"
+
+ //
+ "viewmodel_shake_forward" "0.5"
+ "viewmodel_shake_up" "0.2"
+ "viewmodel_shake_right" "0.0"
+
+ // Bob
+ "bob_cycle_time" "0.45"
+ "bob_vert_dist" "0.1"
+ "bob_horz_dist" "0.1"
+ "bob_max_speed" "150"
+ "bob_pitch" "0.75"
+ "bob_yaw" "0.5"
+ "bob_roll" "-0.75"
+
+ // Bob_Zoomed
+ "bob_cycle_time_zoomed" "0.4"
+ "bob_vert_dist_zoomed" "0.01"
+ "bob_horz_dist_zoomed" "0.01"
+ "bob_max_speed_zoomed" "150"
+ //"bob_pitch_zoomed" "0.002"
+ //"bob_yaw_zoomed" "-.002"
+ //"bob_roll_zoomed" ".002"
+
+ // Rumble
+ "fire_rumble" "rumble_shotgun"
+
+ // Sway
+ "sway_rotate_attach" "SWAY_ROTATE"
+ "sway_min_x" "-0.5"
+ "sway_min_y" "-0.5"
+ "sway_min_z" "-0.6"
+ "sway_max_x" "0.5"
+ "sway_max_y" "0.5"
+ "sway_max_z" "0.6"
+ "sway_min_pitch" "-3"
+ "sway_min_yaw" "-2.5"
+ "sway_min_roll" "-4"
+ "sway_max_pitch" "3"
+ "sway_max_yaw" "2.5"
+ "sway_max_roll" "4"
+ "sway_translate_gain" "2.5"
+ "sway_rotate_gain" "7"
+ "sway_move_forward_translate_x" "-0.1"
+ "sway_move_forward_translate_z" "-0.5"
+ "sway_move_back_translate_x" "0.2"
+ "sway_move_back_translate_z" "-0.2"
+ "sway_move_left_translate_y" "-1"
+ "sway_move_left_translate_z" "-0.5"
+ "sway_move_left_rotate_roll" "-4"
+ "sway_move_right_translate_y" "1"
+ "sway_move_right_translate_z" "-0.5"
+ "sway_move_right_rotate_roll" "4"
+ "sway_move_up_translate_z" "-1"
+ "sway_move_down_translate_z" "1"
+ "sway_turn_left_rotate_yaw" "-2.5"
+ "sway_turn_right_rotate_yaw" "2.5"
+
+ "sway_turn_left_translate_y" ".5"
+ "sway_turn_right_translate_y" "-.5"
+ "sway_turn_up_translate_z" ".2"
+ "sway_turn_down_translate_z" "-.2"
+ "sway_turn_up_translate_x" ".1"
+ "sway_turn_down_translate_x" "-.1"
+
+ "sway_turn_left_rotate_roll" "4"
+ "sway_turn_right_rotate_roll" "-4"
+ "sway_turn_up_rotate_pitch" "3"
+ "sway_turn_down_rotate_pitch" "-3"
+ "sway_turn_up_rotate_roll" "-0.8"
+ "sway_turn_down_rotate_roll" "0.8"
+
+ // Zoomed Sway
+ "sway_rotate_attach_zoomed" "SWAY_ROTATE_ZOOMED"
+ "sway_rotate_attach_blend_time_zoomed" "0.2"
+ "sway_rotate_gain_zoomed" "5"
+
+ "sway_min_yaw_zoomed" "-0.045"
+ "sway_max_yaw_zoomed" "0.045"
+ "sway_turn_left_rotate_yaw_zoomed" "-0.085"
+ "sway_turn_right_rotate_yaw_zoomed" "0.085"
+
+ "sway_min_roll_zoomed" "-4"
+ "sway_max_roll_zoomed" "4"
+ "sway_turn_left_rotate_roll_zoomed" "0"
+ "sway_turn_right_rotate_roll_zoomed" "0"
+
+ "sway_move_right_rotate_roll_zoomed" "0.2"
+ "sway_move_left_rotate_roll_zoomed" "-0.2"
+
+ "sway_min_pitch_zoomed" "-0.03"
+ "sway_max_pitch_zoomed" "0.03"
+ "sway_turn_up_rotate_pitch_zoomed" "0.07"
+ "sway_turn_down_rotate_pitch_zoomed" "-0.07"
+
+ // NPC
+ "proficiency_poor_spreadscale" "7.0"
+ "proficiency_average_spreadscale" "5.0"
+ "proficiency_good_spreadscale" "4.5"
+ "proficiency_very_good_spreadscale" "3.7"
+
+ "npc_min_engage_range" "0"
+ "npc_max_engage_range" "800"
+ "npc_min_engage_range_heavy_armor" "500"
+ "npc_max_engage_range_heavy_armor" "800"
+ "npc_min_range" "0"
+ "npc_max_range" "800"
+
+ "npc_min_burst" "1"
+ "npc_max_burst" "1"
+ "npc_rest_time_between_bursts_min" "0.5"
+ "npc_rest_time_between_bursts_max" "0.7"
+
+ // WeaponED Unhandled Key/Values and custom script Key/Values
+ "bob_tilt_angle" "0.5"
+ "sway_turn_angle_factor" "-0.5"
+ "sway_turn_origin_factor" "0"
+ "sway_turn_angle_factor_zoomed" "0"
+ "sway_turn_origin_factor_zoomed" "0.05"
+ "sway_move_angle_factor" "0.15"
+ "sway_move_origin_factor" "0.15"
+ "sway_move_angle_factor_zoomed" "0"
+ "sway_move_origin_factor_zoomed" "0.03"
+ "sway_gain" "10.0"
+ "deployfirst_time" "1.25"
+ "deploycatch_time" "1.33"
+ "sprintcycle_time" ".55"
+
+
+ "clip_bodygroup" "twinbshotgun_magazine"
+ "clip_bodygroup_index_shown" "0"
+ "clip_bodygroup_index_hidden" "1"
+ "clip_bodygroup_show_for_milestone_0" "1"
+ "clip_bodygroup_show_for_milestone_1" "0"
+ "clip_bodygroup_show_for_milestone_2" "1"
+ "clip_bodygroup_show_for_milestone_3" "1"
+ Mods
+ {
+ iron_sights
+ {
+ }
+ pas_run_and_gun
+ {
+ "primary_fire_does_not_block_sprint" "1"
+ "crosshair_force_sprint_fade_disabled" "1"
+ }
+ pas_fast_ads
+ {
+ //Fast ADS
+ "zoom_time_in" "*0.5"
+ "zoom_time_out" "*0.6"
+ }
+ pas_fast_swap
+ {
+ //Fast Swap
+ "fast_swap_to" "1"
+ }
+ burn_mod_shotgun
+ {
+ "is_burn_mod" "1"
+ "fx_muzzle_flash_view" "P_wpn_muz_shotgun_amp_FP"
+ "fx_muzzle_flash_world" "P_wpn_muz_shotgun_amp"
+ "fx_muzzle_flash_attach" "muzzle_flash"
+ "tracer_effect" "P_wpn_tracer_shotgun_BC"
+
+ "damage_near_value" "250"
+ "damage_far_value" "20"
+ "damage_near_value_titanarmor" "400"
+ "damage_far_value_titanarmor" "20"
+ }
+ }
+
+
+ active_crosshair_count "1"
+ rui_crosshair_index "0"
+
+ RUI_CrosshairData
+ {
+ DefaultArgs
+ {
+ adjustedSpread weapon_spread
+ adsFrac player_zoomFrac
+ isSprinting player_is_sprinting
+ isReloading weapon_is_reloading
+ teamColor crosshair_team_color
+ isAmped weapon_is_amped
+ crosshairMovementX crosshair_movement_x
+ crosshairMovementY crosshair_movement_y
+ }
+
+ Crosshair_1
+ {
+ "ui" "ui/crosshair_shotgun"
+ "base_spread" "-4.0"
+ Args
+ {
+ isFiring weapon_is_firing
+ }
+ }
+ }
+}