diff options
author | GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> | 2024-08-14 17:31:06 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-14 17:31:06 +0200 |
commit | fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385 (patch) | |
tree | 4a78391c5008a4aa50d81fbca5d9f26bb475cd96 /Northstar.Custom/mod/scripts | |
parent | bcec5a5e9edd2a2af3a017ea4b250a9ba1112e6f (diff) | |
parent | 7aa3958ccd8e32970736654dfae0c7a87f0798bb (diff) | |
download | NorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.tar.gz NorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.zip |
Merge branch 'main' into permanent-amped-weapons-fix-pr
Diffstat (limited to 'Northstar.Custom/mod/scripts')
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
+ }
+ }
+ }
+}
|