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.CustomServers/mod/scripts/vscripts/gamemodes | |
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.CustomServers/mod/scripts/vscripts/gamemodes')
13 files changed, 2062 insertions, 313 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut index d6d578bb..4ed7ee4a 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut @@ -1,7 +1,6 @@ global function AiGameModes_Init -global function AiGameModes_SetGruntWeapons -global function AiGameModes_SetSpectreWeapons +global function AiGameModes_SetNPCWeapons global function AiGameModes_SpawnDropShip global function AiGameModes_SpawnDropPod @@ -15,25 +14,20 @@ const INTRO_DROPSHIP_CUTOFF = 2000 struct { - array< string > gruntWeapons = [ "mp_weapon_rspn101" ] - array< string > spectreWeapons = [ "mp_weapon_hemlok_smg" ] + table< string, array<string> > npcWeaponsTable // npcs have their default weapon in aisettings file } file void function AiGameModes_Init() { - } //------------------------------------------------------ -void function AiGameModes_SetGruntWeapons( array< string > weapons ) -{ - file.gruntWeapons = weapons -} - -void function AiGameModes_SetSpectreWeapons( array< string > weapons ) +void function AiGameModes_SetNPCWeapons( string npcClass, array<string> weapons ) { - file.spectreWeapons = weapons + if ( !( npcClass in file.npcWeaponsTable ) ) + file.npcWeaponsTable[ npcClass ] <- [] + file.npcWeaponsTable[ npcClass ] = weapons } //------------------------------------------------------ @@ -59,7 +53,7 @@ void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int c foreach ( guy in guys ) { - ReplaceWeapon( guy, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] ) + SetUpNPCWeapons( guy ) guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) } @@ -68,31 +62,23 @@ void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int c } -void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array<entity> guys ) squadHandler = null ) +void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array<entity> guys ) squadHandler = null, int flags = 0 ) { - string squadName = MakeSquadName( team, UniqueString( "" ) ) - array<entity> guys - entity pod = CreateDropPod( pos, <0,0,0> ) - InitFireteamDropPod( pod ) - + InitFireteamDropPod( pod, flags ) + + waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot ) + + string squadName = MakeSquadName( team, UniqueString( "" ) ) + array<entity> guys for ( int i = 0; i < 4 ;i++ ) { entity npc = CreateNPC( content, team, pos,<0,0,0> ) DispatchSpawn( npc ) SetSquad( npc, squadName ) - switch ( content ) - { - case "npc_soldier": - ReplaceWeapon( npc, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] ) - break - - case "npc_spectre": - ReplaceWeapon( npc, file.spectreWeapons[ RandomInt( file.spectreWeapons.len() ) ], [] ) - break - } + SetUpNPCWeapons( npc ) npc.SetParent( pod, "ATTACH", true ) @@ -100,37 +86,100 @@ void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string guys.append( npc ) } - // The order here is different so we can show on minimap while were still falling + ActivateFireteamDropPod( pod, guys ) + + // start searching for enemies if ( squadHandler != null ) thread squadHandler( guys ) - - waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot ) - - ActivateFireteamDropPod( pod, guys ) } +const float REAPER_WARPFALL_DELAY = 4.7 // same as fd does void function AiGameModes_SpawnReaper( vector pos, vector rot, int team, string aiSettings = "", void functionref( entity reaper ) reaperHandler = null ) { + float reaperLandTime = REAPER_WARPFALL_DELAY + 1.2 // reaper takes ~1.2s to warpfall + thread HotDrop_Spawnpoint( pos, team, reaperLandTime, false, damagedef_reaper_fall ) + + wait REAPER_WARPFALL_DELAY entity reaper = CreateSuperSpectre( team, pos, rot ) + reaper.EndSignal( "OnDestroy" ) + // reaper highlight + Highlight_SetFriendlyHighlight( reaper, "sp_enemy_pilot" ) + reaper.Highlight_SetParam( 1, 0, < 1,1,1 > ) + SetDefaultMPEnemyHighlight( reaper ) + Highlight_SetEnemyHighlight( reaper, "enemy_titan" ) + SetSpawnOption_Titanfall( reaper ) SetSpawnOption_Warpfall( reaper ) if ( aiSettings != "" ) SetSpawnOption_AISettings( reaper, aiSettings ) + HideName( reaper ) // prevent flash a name onto it DispatchSpawn( reaper ) - + + reaper.WaitSignal( "WarpfallComplete" ) + ShowName( reaper ) // show name again after drop if ( reaperHandler != null ) thread reaperHandler( reaper ) } +// copied from cl_replacement_titan_hud.gnut +void function HotDrop_Spawnpoint( vector origin, int team, float impactTime, bool hasFriendlyWarning = false, int damageDef = -1 ) +{ + array<entity> targetEffects = [] + vector surfaceNormal = < 0, 0, 1 > + + int index = GetParticleSystemIndex( $"P_ar_titan_droppoint" ) + + if( hasFriendlyWarning ) + { + entity effectFriendly = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal ) + SetTeam( effectFriendly, team ) + EffectSetControlPointVector( effectFriendly, 1, FRIENDLY_COLOR_FX ) + effectFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY + effectFriendly.DisableHibernation() // prevent it from fading out + targetEffects.append( effectFriendly ) + } + + entity effectEnemy = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal ) + SetTeam( effectEnemy, team ) + EffectSetControlPointVector( effectEnemy, 1, ENEMY_COLOR_FX ) + effectEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY + effectEnemy.DisableHibernation() // prevent it from fading out + targetEffects.append( effectEnemy ) + + // so enemy npcs will mostly avoid them + entity damageAreaInfo + if ( damageDef > -1 ) + { + damageAreaInfo = CreateEntity( "info_target" ) + DispatchSpawn( damageAreaInfo ) + AI_CreateDangerousArea_DamageDef( damageDef, damageAreaInfo, team, true, true ) + } + + wait impactTime + + // clean up + foreach( entity targetEffect in targetEffects ) + { + if ( IsValid( targetEffect ) ) + EffectStop( targetEffect ) + } + if ( IsValid( damageAreaInfo ) ) + damageAreaInfo.Destroy() +} + // including aisettings stuff specifically for at bounty titans +const float TITANFALL_WARNING_DURATION = 5.0 void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string setFile, string aiSettings = "", void functionref( entity titan ) titanHandler = null ) { entity titan = CreateNPCTitan( setFile, TEAM_BOTH, pos, rot ) SetSpawnOption_Titanfall( titan ) SetSpawnOption_Warpfall( titan ) + + // modified: do a hotdrop spawnpoint warning + thread HotDrop_Spawnpoint( pos, team, TITANFALL_WARNING_DURATION, false, damagedef_titan_fall ) if ( aiSettings != "" ) SetSpawnOption_AISettings( titan, aiSettings ) @@ -141,12 +190,27 @@ void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string s thread titanHandler( titan ) } -// entity.ReplaceActiveWeapon gave grunts archers sometimes, this is my replacement for it -void function ReplaceWeapon( entity guy, string weapon, array<string> mods ) +void function SetUpNPCWeapons( entity guy ) { - guy.TakeActiveWeapon() - guy.GiveWeapon( weapon, mods ) - guy.SetActiveWeaponByName( weapon ) + string className = guy.GetClassName() + + array<string> mainWeapons + if ( className in file.npcWeaponsTable ) + mainWeapons = file.npcWeaponsTable[ className ] + + if ( mainWeapons.len() == 0 ) // no valid weapons + return + + // take off existing main weapons, or sometimes they'll have a archer by default + foreach ( entity weapon in guy.GetMainWeapons() ) + guy.TakeWeapon( weapon.GetWeaponClassName() ) + + if ( mainWeapons.len() > 0 ) + { + string weaponName = mainWeapons[ RandomInt( mainWeapons.len() ) ] + guy.GiveWeapon( weaponName ) + guy.SetActiveWeaponByName( weaponName ) + } } // Checks if we can spawn a dropship at a node, this should guarantee dropship ziplines diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut index 9a94b848..7d73c926 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut @@ -1,22 +1,36 @@ untyped global function GamemodeAITdm_Init -const SQUADS_PER_TEAM = 3 +// these are now default settings +const int SQUADS_PER_TEAM = 4 -const REAPERS_PER_TEAM = 2 +const int REAPERS_PER_TEAM = 2 -const LEVEL_SPECTRES = 125 -const LEVEL_STALKERS = 380 -const LEVEL_REAPERS = 500 +const int LEVEL_SPECTRES = 125 +const int LEVEL_STALKERS = 380 +const int LEVEL_REAPERS = 500 + +// add settings +global function AITdm_SetSquadsPerTeam +global function AITdm_SetReapersPerTeam +global function AITdm_SetLevelSpectres +global function AITdm_SetLevelStalkers +global function AITdm_SetLevelReapers struct { // Due to team based escalation everything is an array - array< int > levels = [ LEVEL_SPECTRES, LEVEL_SPECTRES ] + array< int > levels = [] // Initilazed in `Spawner_Threaded` array< array< string > > podEntities = [ [ "npc_soldier" ], [ "npc_soldier" ] ] array< bool > reapers = [ false, false ] -} file + // default settings + int squadsPerTeam = SQUADS_PER_TEAM + int reapersPerTeam = REAPERS_PER_TEAM + int levelSpectres = LEVEL_SPECTRES + int levelStalkers = LEVEL_STALKERS + int levelReapers = LEVEL_REAPERS +} file void function GamemodeAITdm_Init() { @@ -34,18 +48,48 @@ void function GamemodeAITdm_Init() if ( GetCurrentPlaylistVarInt( "aitdm_archer_grunts", 0 ) == 0 ) { - AiGameModes_SetGruntWeapons( [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_hemlok_smg", "mp_weapon_lstar", "mp_weapon_mastiff" ] ) } else { - AiGameModes_SetGruntWeapons( [ "mp_weapon_rocket_launcher" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_rocket_launcher" ] ) } ScoreEvent_SetupEarnMeterValuesForMixedModes() + SetupGenericTDMChallenge() } +// add settings +void function AITdm_SetSquadsPerTeam( int squads ) +{ + file.squadsPerTeam = squads +} + +void function AITdm_SetReapersPerTeam( int reapers ) +{ + file.reapersPerTeam = reapers +} + +void function AITdm_SetLevelSpectres( int level ) +{ + file.levelSpectres = level +} + +void function AITdm_SetLevelStalkers( int level ) +{ + file.levelStalkers = level +} + +void function AITdm_SetLevelReapers( int level ) +{ + file.levelReapers = level +} +// + // Starts skyshow, this also requiers AINs but doesn't crash if they're missing void function OnPrematchStart() { @@ -74,10 +118,12 @@ void function HandleScoreEvent( entity victim, entity attacker, var damageInfo ) // Basic checks if ( victim == attacker || !( attacker.IsPlayer() || attacker.IsTitan() ) || GetGameState() != eGameState.Playing ) return - // Hacked spectre filter if ( victim.GetOwner() == attacker ) return + // NPC titans without an owner player will not count towards any team's score + if ( attacker.IsNPC() && attacker.IsTitan() && !IsValid( GetPetTitanOwner( attacker ) ) ) + return // Split score so we can check if we are over the score max // without showing the wrong value on client @@ -189,7 +235,7 @@ void function SpawnIntroBatch_Threaded( int team ) int ships = shipNodes.len() - for ( int i = 0; i < SQUADS_PER_TEAM; i++ ) + for ( int i = 0; i < file.squadsPerTeam; i++ ) { if ( pods != 0 || ships == 0 ) { @@ -234,6 +280,7 @@ void function Spawner_Threaded( int team ) // used to index into escalation arrays int index = team == TEAM_MILITIA ? 0 : 1 + file.levels = [ file.levelSpectres, file.levelSpectres ] // due we added settings, should init levels here! while( true ) { @@ -248,7 +295,7 @@ void function Spawner_Threaded( int team ) if ( file.reapers[ index ] ) { array< entity > points = SpawnPoints_GetDropPod() - if ( reaperCount < REAPERS_PER_TEAM ) + if ( reaperCount < file.reapersPerTeam ) { entity node = points[ GetSpawnPointIndex( points, team ) ] waitthread AiGameModes_SpawnReaper( node.GetOrigin(), node.GetAngles(), team, "npc_super_spectre_aitdm", ReaperHandler ) @@ -256,7 +303,7 @@ void function Spawner_Threaded( int team ) } // NORMAL SPAWNS - if ( count < SQUADS_PER_TEAM * 4 - 2 ) + if ( count < file.squadsPerTeam * 4 - 2 ) { string ent = file.podEntities[ index ][ RandomInt( file.podEntities[ index ].len() ) ] @@ -302,19 +349,19 @@ void function Escalate( int team ) // Based on score escalate a team switch ( file.levels[ index ] ) { - case LEVEL_SPECTRES: - file.levels[ index ] = LEVEL_STALKERS + case file.levelSpectres: + file.levels[ index ] = file.levelStalkers file.podEntities[ index ].append( "npc_spectre" ) SetGlobalNetInt( defcon, 2 ) return - case LEVEL_STALKERS: - file.levels[ index ] = LEVEL_REAPERS + case file.levelStalkers: + file.levels[ index ] = file.levelReapers file.podEntities[ index ].append( "npc_stalker" ) SetGlobalNetInt( defcon, 3 ) return - case LEVEL_REAPERS: + case file.levelReapers: file.reapers[ index ] = true SetGlobalNetInt( defcon, 4 ) return @@ -351,30 +398,47 @@ int function GetSpawnPointIndex( array< entity > points, int team ) // AI can also flee deeper into their zone suggesting someone spent way too much time on this void function SquadHandler( array<entity> guys ) { + int team = guys[0].GetTeam() + // show the squad enemy radar + array<entity> players = GetPlayerArrayOfEnemies( team ) + foreach ( entity guy in guys ) + { + if ( IsAlive( guy ) ) + { + foreach ( player in players ) + guy.Minimap_AlwaysShow( 0, player ) + } + } + // Not all maps have assaultpoints / have weird assault points ( looking at you ac ) // So we use enemies with a large radius - array< entity > points = GetNPCArrayOfEnemies( guys[0].GetTeam() ) - - if ( points.len() == 0 ) + while ( GetNPCArrayOfEnemies( team ).len() == 0 ) // if we can't find any enemy npcs, keep waiting + WaitFrame() + + // our waiting is end, check if any soldiers left + bool squadAlive = false + foreach ( entity guy in guys ) + { + if ( IsAlive( guy ) ) + squadAlive = true + else + guys.removebyvalue( guy ) + } + if ( !squadAlive ) return + + array<entity> points = GetNPCArrayOfEnemies( team ) vector point point = points[ RandomInt( points.len() ) ].GetOrigin() - array<entity> players = GetPlayerArrayOfEnemies( guys[0].GetTeam() ) - - // Setup AI + // Setup AI, first assault point foreach ( guy in guys ) { guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) guy.AssaultPoint( point ) guy.AssaultSetGoalRadius( 1600 ) // 1600 is minimum for npc_stalker, works fine for others - - // show on enemy radar - foreach ( player in players ) - guy.Minimap_AlwaysShow( 0, player ) - - + //thread AITdm_CleanupBoredNPCThread( guy ) } @@ -392,16 +456,32 @@ void function SquadHandler( array<entity> guys ) // Stop func if our squad has been killed off if ( guys.len() == 0 ) return + } + + // Get point and send our whole squad to it + points = GetNPCArrayOfEnemies( team ) + if ( points.len() == 0 ) // can't find any points here + { + // Have to wait some amount of time before continuing + // because if we don't the server will continue checking this + // forever, aren't loops fun? + // This definitely didn't waste ~8 hours of my time reverting various + // launcher PRs before finding this mods PR that caused servers to + // freeze forever before having their process killed by the dedi watchdog + // without any logging. If anyone reads this, PLEASE add logging to your scripts + // for when weird edge cases happen, it can literally only help debugging. -Spoon + WaitFrame() + continue + } - // Get point and send guy to it - points = GetNPCArrayOfEnemies( guy.GetTeam() ) - if ( points.len() == 0 ) - continue - - point = points[ RandomInt( points.len() ) ].GetOrigin() - - guy.AssaultPoint( point ) + point = points[ RandomInt( points.len() ) ].GetOrigin() + + foreach ( guy in guys ) + { + if ( IsAlive( guy ) ) + guy.AssaultPoint( point ) } + wait RandomFloatRange(5.0,15.0) } } @@ -507,4 +587,4 @@ void function AITdm_CleanupBoredNPCThread( entity guy ) print( "cleaning up bored npc: " + guy + " from team " + guy.GetTeam() ) guy.Destroy() -}
\ No newline at end of file +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut index 915e03e0..9cf0021d 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut @@ -1,12 +1,54 @@ +untyped // AddCallback_OnUseEntity() needs this global function GamemodeAt_Init global function RateSpawnpoints_AT -const int BH_AI_TEAM = TEAM_BOTH -const int BOUNTY_TITAN_DAMAGE_POOL = 400 // Rewarded for damage -const int BOUNTY_TITAN_KILL_REWARD = 100 // Rewarded for kill -const float WAVE_STATE_TRANSITION_TIME = 5.0 +// Old bobr note which still applies after a year :) +// IMPLEMENTATION NOTES: +// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives) +// in the interest of time, this script isn't gonna support any of that atm +// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead +// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience + + +// Bank settings +const float AT_BANKS_OPEN_DURATION = 45.0 // Bank open time +const int AT_BANK_DEPOSIT_RATE = 10 // Amount deposited per second +const int AT_BANK_DEPOSIT_RADIUS = 256 // bank radius for depositing +const float AT_BANK_FORCE_CLOSE_DELAY = 4.0 // If all bonus money has been deposited close the banks after this constant early + +// TODO: The reference function no longer exists, check if this still holds true +// VoyageDB: HACK score events... respawn made things in AT_SetScoreEventOverride() really messed up, have to do some hack here +const array<string> AT_ENABLE_SCOREEVENTS = +[ + // these are disabled in AT_SetScoreEventOverride(), but related scoreEvents are not implemented into gamemode + // needs to re-enable them + "DoomTitan", + "DoomAutoTitan" +] +const array<string> AT_DISABLE_SCOREEVENTS = +[ + // these are missed in AT_SetScoreEventOverride(), but game actually used them + // needs to disable them + "KillStalker" +] + +// Wave settings +// General +const int AT_AI_TEAM = TEAM_BOTH // Allow AI to attack and be attacked by both player teams +const float AT_FIRST_WAVE_START_DELAY = 10.0 // First wave has an extra delay before begining +const float AT_WAVE_TRANSITION_DELAY = 5.0 // Time between each wave and banks opening/closing +const float AT_WAVE_END_ANNOUNCEMENT_DELAY = 1.0 // Extra wait before announcing wave cleaned + +// Squad settings +const int AT_DROPPOD_SQUADS_ALLOWED_ON_FIELD = 4 // default is 4 droppod squads on field, won't use if AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK turns on // TODO: verify this -const array<string> VALID_BOUNTY_TITAN_SETTINGS = [ +// Titan bounty settings +const float AT_BOUNTY_TITAN_CHECK_DELAY = 10.0 // wait for bounty titans landing before we start checking their life state +const float AT_BOUNTY_TITAN_HEALTH_MULTIPLIER = 3 // TODO: Verify this + +// Titan boss settings, check sh_gamemode_at.nut for more info +const array<string> AT_BOUNTY_TITANS_AI_SETTINGS = +[ "npc_titan_atlas_stickybomb_bounty", "npc_titan_atlas_tracker_bounty", "npc_titan_ogre_minigun_bounty", @@ -16,102 +58,582 @@ const array<string> VALID_BOUNTY_TITAN_SETTINGS = [ "npc_titan_atlas_vanguard_bounty" ] +// Extra +// Respawn didn't use the "totalAllowedOnField" for npc spawning, they only allow 1 squad to be on field for each type of npc. enabling this might cause too much npcs spawning and crash the game +const bool AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK = false -// IMPLEMENTATION NOTES: -// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives) -// in the interest of time, this script isn't gonna support any of that atm -// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead -// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience +// Objectives +const int AT_OBJECTIVE_EMPTY = -1 // Remove objective +const int AT_OBJECTIVE_KILL_DZ = 104 // #AT_OBJECTIVE_KILL_DZ +const int AT_OBJECTIVE_KILL_DZ_MULTI = 105 // #AT_OBJECTIVE_KILL_DZ_MULTI +const int AT_OBJECTIVE_KILL_BOSS = 106 // #AT_OBJECTIVE_KILL_BOSS +const int AT_OBJECTIVE_KILL_BOSS_MULTI = 107 // #AT_OBJECTIVE_KILL_BOSS_MULTI +const int AT_OBJECTIVE_BANK_OPEN = 109 // #AT_BANK_OPEN_OBJECTIVE -struct { - array<entity> campsToRegisterOnEntitiesDidLoad +// When a player tries to deposit when they have 0 bonus money +// we show a help mesage, this is the ratelimit for that message +// so that we dont spam it too much +const float AT_PLAYER_HUD_MESSAGE_COOLDOWN = 2.5 +// Due to bad navmeshes NPCs may wonder off to bumfuck nowhere or the game +// might teleport them into the map while trying to correct their position +// This obviously breaks bounty hunt where the objective is to kill ALL ai +// so we try to cleanup the camps after a set amount of time of inactivity +const int AT_CAMP_BORED_NPCS_LEFT_TO_START_CLEANUP = 3 +const float AT_CAMP_BORED_CLEANUP_WAIT = 60.0 +struct +{ array<entity> banks array<AT_WaveOrigin> camps + + // Used to track ScriptmanagedEntArrays of ai squads + table< int, array<int> > campScriptEntArrays - table< int, table< string, int > > trackedCampNPCSpawns + table< entity, bool > titanIsBountyBoss + table< entity, int > bountyTitanRewards + table< entity, int > npcStolenBonus + table< entity, bool > playerBankUploading + table< entity, table<entity, int> > playerSavedBountyDamage + table< entity, float > playerHudMessageAllowedTime } file void function GamemodeAt_Init() { - AddCallback_GameStateEnter( eGameState.Playing, RunATGame ) - + // wave + RegisterSignal( "ATWaveEnd" ) + // camp + RegisterSignal( "ATCampClean" ) + RegisterSignal( "ATAllCampsClean" ) + + // Set-up score callbacks + ScoreEvent_SetupEarnMeterValuesForMixedModes() + AddCallback_OnPlayerKilled( AT_PlayerOrNPCKilledScoreEvent ) + AddCallback_OnNPCKilled( AT_PlayerOrNPCKilledScoreEvent ) + + // Set npc weapons + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_hemlok_smg", "mp_weapon_lstar", "mp_weapon_mastiff" ] ) + + // Gamestate callbacks + AddCallback_GameStateEnter( eGameState.Prematch, OnATGamePrematch ) + AddCallback_GameStateEnter( eGameState.Playing, OnATGamePlaying ) + + // Initilaze player AddCallback_OnClientConnected( InitialiseATPlayer ) - AddSpawnCallbackEditorClass( "info_target", "info_attrition_bank", CreateATBank ) - AddSpawnCallbackEditorClass( "info_target", "info_attrition_camp", CreateATCamp ) - AddCallback_EntitiesDidLoad( CreateATCamps_Delayed ) + // Initilaze gamemode entities + AddCallback_EntitiesDidLoad( OnEntitiesDidLoad ) } void function RateSpawnpoints_AT( int checkclass, array<entity> spawnpoints, int team, entity player ) { - RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp + RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) } -// world and player inits + + +//////////////////////////////////////// +///// GAMESTATE CALLBACK FUNCTIONS ///// +//////////////////////////////////////// + +void function OnATGamePrematch() +{ + AT_ScoreEventsValueSetUp() +} + +void function OnATGamePlaying() +{ + thread AT_GameLoop_Threaded() +} + +//////////////////////////////////////////// +///// GAMESTATE CALLBACK FUNCTIONS END ///// +//////////////////////////////////////////// + + + +//////////////////////////// +///// PLAYER FUNCTIONS ///// +//////////////////////////// void function InitialiseATPlayer( entity player ) { Remote_CallFunction_NonReplay( player, "ServerCallback_AT_OnPlayerConnected" ) + player.SetPlayerNetInt( "AT_bonusPointMult", 1 ) + file.playerBankUploading[ player ] <- false + file.playerSavedBountyDamage[ player ] <- {} + file.playerHudMessageAllowedTime[ player ] <- 0.0 + thread AT_PlayerTitleThink( player ) + thread AT_PlayerObjectiveThink( player ) } -void function CreateATBank( entity spawnpoint ) +void function AT_PlayerTitleThink( entity player ) { - entity bank = CreatePropDynamic( spawnpoint.GetModelName(), spawnpoint.GetOrigin(), spawnpoint.GetAngles(), SOLID_VPHYSICS ) - bank.SetScriptName( "AT_Bank" ) - - // create tracker ent - // we don't need to store these at all, client just needs to get them - DispatchSpawn( GetAvailableBankTracker( bank ) ) + player.EndSignal( "OnDestroy" ) + + while ( true ) + { + if ( GetGameState() == eGameState.Playing ) + { + // Set player money count + player.SetTitle( "$" + string( AT_GetPlayerBonusPoints( player ) ) ) + + if( AT_GetPlayerBonusPoints( player ) >= 600 && !HasPlayerCompletedMeritScore( player ) ) //Challenge is: "Earn $600." + { + AddPlayerScore( player, "ChallengeATAssault" ) + SetPlayerChallengeMeritScore( player ) + } + } + else if ( GetGameState() >= eGameState.WinnerDetermined ) + { + if ( player.IsTitan() ) + player.SetTitle( GetTitanPlayerTitle( player ) ) + else + player.SetTitle( "" ) + + return + } + + WaitFrame() + } +} + +string function GetTitanPlayerTitle( entity player ) +{ + entity soul = player.GetTitanSoul() + + if ( !IsValid( soul ) ) + return "" - thread PlayAnim( bank, "mh_inactive_idle" ) + string settings = GetSoulPlayerSettings( soul ) + var title = GetPlayerSettingsFieldForClassName( settings, "printname" ) + + if ( title == null ) + return "" - file.banks.append( bank ) + return expect string( title ) +} + +void function AT_PlayerObjectiveThink( entity player ) +{ + player.EndSignal( "OnDestroy" ) + + int curObjective = AT_OBJECTIVE_EMPTY + while ( true ) + { + // game entered other state + if ( GetGameState() >= eGameState.WinnerDetermined ) + { + player.SetPlayerNetInt( "gameInfoStatusText", AT_OBJECTIVE_EMPTY ) + return + } + + int nextObjective = AT_OBJECTIVE_EMPTY + + // Determine objective text for player + if ( !IsAlive( player ) ) // Don't show objective to dead players + { + nextObjective = AT_OBJECTIVE_EMPTY + } + else // We're still alive + { + if ( GetGlobalNetBool( "banksOpen" ) ) + { + nextObjective = AT_OBJECTIVE_BANK_OPEN + } + else if ( GetGlobalNetBool( "preBankPhase" ) ) + { + nextObjective = AT_OBJECTIVE_EMPTY + } + else + { + // No checks have passed, try to do a "Kill all x near the marked dropzone" objective + int dropZoneActiveCount = 0 + int bossAliveCount = 0 + array<entity> campEnts + campEnts.append( GetGlobalNetEnt( "camp1Ent" ) ) + campEnts.append( GetGlobalNetEnt( "camp2Ent" ) ) + + foreach ( entity ent in campEnts ) + { + if ( IsValid( ent ) ) + { + if ( ent.IsTitan() ) + bossAliveCount += 1 + else + dropZoneActiveCount += 1 + } + } + + switch( dropZoneActiveCount ) + { + case 1: + nextObjective = AT_OBJECTIVE_KILL_DZ + break + case 2: + nextObjective = AT_OBJECTIVE_KILL_DZ_MULTI + break + } + + switch( bossAliveCount ) + { + case 1: + nextObjective = AT_OBJECTIVE_KILL_BOSS + break + case 2: + nextObjective = AT_OBJECTIVE_KILL_BOSS_MULTI + break + } + + // We couldn't get an objective, set it to empty + if ( dropZoneActiveCount == 0 && bossAliveCount == 0 ) + nextObjective = AT_OBJECTIVE_EMPTY + } + } + + // Set the objective when changed + if ( curObjective != nextObjective ) + { + player.SetPlayerNetInt( "gameInfoStatusText", nextObjective ) + curObjective = nextObjective + } + + WaitFrame() + } } -void function CreateATCamp( entity spawnpoint ) +//////////////////////////////// +///// PLAYER FUNCTIONS END ///// +//////////////////////////////// + + + +//////////////////////////////////////// +///// GAMEMODE INITILAZE FUNCTIONS ///// +//////////////////////////////////////// + +void function OnEntitiesDidLoad() { - // delay this so we don't do stuff before all spawns are initialised and that - file.campsToRegisterOnEntitiesDidLoad.append( spawnpoint ) + foreach ( entity info_target in GetEntArrayByClass_Expensive( "info_target" ) ) + { + if( info_target.HasKey( "editorclass" ) ) + { + switch( info_target.kv.editorclass ) + { + case "info_attrition_bank": + entity bank = CreateEntity( "prop_script" ) + bank.SetScriptName( "AT_Bank" ) // VoyageDB: don't know how to make client able to track it + bank.SetOrigin( info_target.GetOrigin() ) + bank.SetAngles( info_target.GetAngles() ) + DispatchSpawn( bank ) + bank.kv.solid = SOLID_VPHYSICS + bank.SetModel( info_target.GetModelName() ) + + // Minimap icon init + bank.Minimap_SetCustomState( eMinimapObject_prop_script.AT_BANK ) + bank.Minimap_SetAlignUpright( true ) + bank.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + bank.Minimap_Hide( TEAM_IMC, null ) + bank.Minimap_Hide( TEAM_MILITIA, null ) + + // Create tracker ent + // we don't need to store these at all, client just needs to get them + DispatchSpawn( GetAvailableBankTracker( bank ) ) + + // Make sure the bank is in it's disabled pose + thread PlayAnim( bank, "mh_inactive_idle" ) + // Set the bank usable + AddCallback_OnUseEntity( bank, OnPlayerUseBank ) + bank.SetUsable() + bank.SetUsePrompts( "#AT_USE_BANK_CLOSED", "#AT_USE_BANK_CLOSED" ) + + file.banks.append( bank ) + break; + case "info_attrition_camp": + AT_WaveOrigin campStruct + campStruct.ent = info_target + campStruct.origin = info_target.GetOrigin() + campStruct.radius = expect string( info_target.kv.radius ).tofloat() + campStruct.height = expect string( info_target.kv.height ).tofloat() + + // Assumes every info_attrition_camp will have all 9 phases, possibly not a good idea? + // TODO: verify this on all vanilla maps before release + for ( int i = 0; i < 9; i++ ) + campStruct.phaseAllowed.append( expect string( info_target.kv[ "phase_" + ( i + 1 ) ] ) == "1" ) + + // Get droppod spawns within the camp + foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) + { + vector campPos = info_target.GetOrigin() + vector spawnPos = spawnpoint.GetOrigin() + if ( Distance( campPos, spawnPos ) < campStruct.radius ) + campStruct.dropPodSpawnPoints.append( spawnpoint ) + } + + // Get titan spawns within the camp + foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) + { + vector campPos = info_target.GetOrigin() + vector spawnPos = spawnpoint.GetOrigin() + if ( Distance( campPos, spawnPos ) < campStruct.radius ) + campStruct.titanSpawnPoints.append( spawnpoint ) + } + + file.camps.append( campStruct ) + break; + } + } + } +} + +//////////////////////////////////////////// +///// GAMEMODE INITILAZE FUNCTIONS END ///// +//////////////////////////////////////////// + + + +///////////////////////////// +///// SCORING FUNCTIONS ///// +///////////////////////////// + +// TODO: Don't reward in postmatch +// TODO: Dropping a titan on a bounty with it's dome-shield still up rewards you the bonus, but +// it doesn't actually damage the bounty titan + +void function AT_ScoreEventsValueSetUp() +{ + ScoreEvent_SetEarnMeterValues( "KillTitan", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillAutoTitan", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "AttritionTitanKilled", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillPilot", 0.10, 0.10 ) + ScoreEvent_SetEarnMeterValues( "AttritionPilotKilled", 0.10, 0.10 ) + ScoreEvent_SetEarnMeterValues( "AttritionBossKilled", 0.10, 0.20 ) + ScoreEvent_SetEarnMeterValues( "AttritionGruntKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionSpectreKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionStalkerKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionSuperSpectreKilled", 0.10, 0.10, 0.5 ) + + // HACK + foreach ( string eventName in AT_ENABLE_SCOREEVENTS ) + ScoreEvent_Enable( GetScoreEvent( eventName ) ) + + foreach ( string eventName in AT_DISABLE_SCOREEVENTS ) + ScoreEvent_Disable( GetScoreEvent( eventName ) ) } -void function CreateATCamps_Delayed() +void function AT_PlayerOrNPCKilledScoreEvent( entity victim, entity attacker, var damageInfo ) { - // we delay registering camps until EntitiesDidLoad since they rely on spawnpoints and stuff, which might not all be ready in the creation callback - // unsure if this would be an issue in practice, but protecting against it in case it would be - foreach ( entity camp in file.campsToRegisterOnEntitiesDidLoad ) + if ( !IsValid( attacker ) ) + return + + // Suicide + if ( attacker == victim ) { - AT_WaveOrigin campStruct - campStruct.ent = camp - campStruct.origin = camp.GetOrigin() - campStruct.radius = expect string( camp.kv.radius ).tofloat() - campStruct.height = expect string( camp.kv.height ).tofloat() + if ( victim.IsPlayer() ) + AT_PlayerBonusLoss( victim, AT_GetPlayerBonusPoints( victim ) / 2 ) - // assumes every info_attrition_camp will have all 9 phases, possibly not a good idea? - for ( int i = 0; i < 9; i++ ) - campStruct.phaseAllowed.append( expect string( camp.kv[ "phase_" + ( i + 1 ) ] ) == "1" ) + return + } + + // NPC is the attacker + if ( !attacker.IsPlayer() ) + { + if ( attacker.IsTitan() && IsValid( GetPetTitanOwner( attacker ) ) ) // Re-asign attacker + attacker = GetPetTitanOwner( attacker ) + else // NPC steals money from player, killing it will award the stolen bonus + normal reward + AT_NPCTryStealBonusPoints( attacker, victim ) - // get droppod spawns - foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) - if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) - campStruct.dropPodSpawnPoints.append( spawnpoint ) + return + } + + // Get event name + string eventName = GetAttritionScoreEventName( victim.GetClassName() ) + + if ( victim.IsTitan() ) // titan specific + eventName = GetAttritionScoreEventNameFromAI( victim ) + + if ( eventName == "" ) // no valid scoreEvent + return + + int scoreVal = ScoreEvent_GetPointValue( GetScoreEvent( eventName ) ) + + // pet titan check + if ( victim.IsTitan() && IsValid( GetPetTitanOwner( victim ) ) ) + { + if( GetPetTitanOwner( victim ) == attacker ) // Player ejected + return - foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) - if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) - campStruct.titanSpawnPoints.append( spawnpoint ) + if( GetPetTitanOwner( victim ).IsPlayer() ) // Killed player npc titan + return + + scoreVal = ATTRITION_SCORE_TITAN_MIN + } + + // killed npc + if ( victim.IsNPC() ) + { + int bonusFromNPC = 0 + // If NPC was carrying a bonus award it to the attacker + if ( victim in file.npcStolenBonus ) + { + bonusFromNPC = file.npcStolenBonus[ victim ] + delete file.npcStolenBonus[ victim ] + } + AT_AddPlayerBonusPointsForEntityKilled( attacker, scoreVal, damageInfo, bonusFromNPC ) + AddPlayerScore( attacker, eventName ) // we add scoreEvent here, since basic score events has been overwrited by sh_gamemode_at.nut + // update score difference and scoreboard + AT_AddToPlayerTeamScore( attacker, scoreVal ) + } + + // bonus stealing check + if ( victim.IsPlayer() ) + AT_PlayerTryStealBonusPoints( attacker, victim, damageInfo ) +} + +bool function AT_NPCTryStealBonusPoints( entity attacker, entity victim ) +{ + // basic checks + if ( !attacker.IsNPC() ) + return false - // todo: turret spawns someday maybe + if ( !victim.IsPlayer() ) + return false + + int victimBonus = AT_GetPlayerBonusPoints( victim ) + int bonusToSteal = victimBonus / 2 // npc always steal half the bonus from player, no extra bonus for killing the player + if ( bonusToSteal == 0 ) // player has no bonus! + return false + + if ( !( attacker in file.npcStolenBonus ) ) // init + file.npcStolenBonus[ attacker ] <- 0 - file.camps.append( campStruct ) + file.npcStolenBonus[ attacker ] += bonusToSteal + + AT_PlayerBonusLoss( victim, bonusToSteal ) // tell victim of bonus stolen + + if ( !( attacker in file.titanIsBountyBoss ) ) // if attacker npc is not a bounty titan, we make them highlighted + NPCBountyStolenHighlight( attacker ) + + return true +} + +void function NPCBountyStolenHighlight( entity npc ) +{ + Highlight_SetEnemyHighlight( npc, "enemy_boss_bounty" ) +} + +bool function AT_PlayerTryStealBonusPoints( entity attacker, entity victim, var damageInfo ) +{ + // basic checks + if ( !attacker.IsPlayer() ) + return false + + if ( !victim.IsPlayer() ) + return false + + int victimBonus = AT_GetPlayerBonusPoints( victim ) + + int minScoreCanSteal = ATTRITION_SCORE_PILOT_MIN + if ( victim.IsTitan() ) + minScoreCanSteal = ATTRITION_SCORE_TITAN_MIN + + int bonusToSteal = victimBonus / 2 + int attackerScore = bonusToSteal + bool realStealBonus = true + if ( bonusToSteal <= minScoreCanSteal ) // no enough bonus to steal + { + attackerScore = minScoreCanSteal // give attacker min bonus + realStealBonus = false // we don't do attacker steal events below, just half victim's bonus } + + // servercallback + int victimEHandle = victim.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + // only do attacker events if victim has enough bonus to steal + if ( realStealBonus ) + { + Remote_CallFunction_NonReplay( + attacker, + "ServerCallback_AT_PlayerKillScorePopup", + bonusToSteal, // stolenScore + victimEHandle, // victimEHandle + damageOrigin.x, // x + damageOrigin.y, // y + damageOrigin.z // z + ) + } + else // otherwise we do a normal entity killed scoreEvent + { + AT_AddPlayerBonusPointsForEntityKilled( attacker, attackerScore, damageInfo ) + } + + // update score difference and scoreboard + AT_AddToPlayerTeamScore( attacker, minScoreCanSteal ) + + // steal bonus + // only do attacker events if victim has enough bonus to steal + if ( realStealBonus ) + { + AT_AddPlayerBonusPoints( attacker, bonusToSteal ) + AddPlayerScore( attacker, "AttritionBonusStolen" ) + } + + // tell victim of bonus stolen + AT_PlayerBonusLoss( victim, bonusToSteal ) - file.campsToRegisterOnEntitiesDidLoad.clear() + return realStealBonus } -// scoring funcs +void function AT_PlayerBonusLoss( entity player, int bonusLoss ) +{ + AT_AddPlayerBonusPoints( player, -bonusLoss ) + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_ShowStolenBonus", + bonusLoss // stolenScore + ) +} + +// team score meter +void function AT_AddToPlayerTeamScore( entity player, int amount ) +{ + // do not award any score after the match is ended + if ( GetGameState() > eGameState.Playing ) + return + + // add to scoreboard + player.AddToPlayerGameStat( PGS_ASSAULT_SCORE, amount ) + + // Check score so we dont go over max + if ( GameRules_GetTeamScore(player.GetTeam()) + amount > GetScoreLimit_FromPlaylist() ) + { + amount = GetScoreLimit_FromPlaylist() - GameRules_GetTeamScore(player.GetTeam()) + } + + // update score difference + AddTeamScore( player.GetTeam(), amount ) +} + +// bonus points, players earn from killing +void function AT_AddPlayerBonusPoints( entity player, int amount ) +{ + // do not award any score after the match is ended + if ( GetGameState() > eGameState.Playing ) + return + + // add to scoreboard + player.AddToPlayerGameStat( PGS_SCORE, amount ) + AT_SetPlayerBonusPoints( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount ) +} + +int function AT_GetPlayerBonusPoints( entity player ) +{ + return player.GetPlayerNetInt( "AT_bonusPoints" ) + player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 +} -// don't use this where possible as it doesn't set score and stuff -void function AT_SetPlayerCash( entity player, int amount ) +void function AT_SetPlayerBonusPoints( entity player, int amount ) { // split into stacks of 256 where necessary int stacks = amount / 256 // automatically rounds down because int division @@ -120,198 +642,986 @@ void function AT_SetPlayerCash( entity player, int amount ) player.SetPlayerNetInt( "AT_bonusPoints", amount - stacks * 256 ) } -void function AT_AddPlayerCash( entity player, int amount ) +// total points, the value player actually uploaded to team score +void function AT_AddPlayerTotalPoints( entity player, int amount ) { - // update score difference - AddTeamScore( player.GetTeam(), amount / 2 ) - AT_SetPlayerCash( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount ) + // update score difference and scoreboard, calling this function meaning player has deposited their bonus to team score + AT_AddToPlayerTeamScore( player, amount ) + AT_SetPlayerTotalPoints( player, player.GetPlayerNetInt( "AT_totalPoints" ) + ( player.GetPlayerNetInt( "AT_totalPoints256" ) * 256 ) + amount ) +} + +void function AT_SetPlayerTotalPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_totalPoints256", stacks ) + player.SetPlayerNetInt( "AT_totalPoints", amount - stacks * 256 ) +} + +// earn points, seems not used +void function AT_AddPlayerEarnedPoints( entity player, int amount ) +{ + AT_SetPlayerBonusPoints( player, player.GetPlayerNetInt( "AT_earnedPoints" ) + ( player.GetPlayerNetInt( "AT_earnedPoints256" ) * 256 ) + amount ) +} + +void function AT_SetPlayerEarnedPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_earnedPoints256", stacks ) + player.SetPlayerNetInt( "AT_earnedPoints", amount - stacks * 256 ) } -// run gamestate +// damaging bounty +void function AT_AddPlayerBonusPointsForBossDamaged( entity player, entity victim, int amount, var damageInfo ) +{ + AT_AddPlayerBonusPoints( player, amount ) + // update score difference and scoreboard + AT_AddToPlayerTeamScore( player, amount ) + + // send servercallback for damaging + int bossEHandle = victim.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) -void function RunATGame() + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_BossDamageScorePopup", + amount, // damageScore + amount, // damageBonus + bossEHandle, // bossEHandle + damageOrigin.x, // x + damageOrigin.y, // y + damageOrigin.z // z + ) +} + +void function AT_AddPlayerBonusPointsForEntityKilled( entity player, int amount, var damageInfo, int extraBonus = 0 ) { - thread RunATGame_Threaded() + AT_AddPlayerBonusPoints( player, amount + extraBonus ) + + // send servercallback for damaging + int attackerEHandle = player.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_ShowATScorePopup", + attackerEHandle, // attackerEHandle + amount, // damageScore + amount + extraBonus, // damageBonus + damageOrigin.x, // damagePosX + damageOrigin.y, // damagePosX + damageOrigin.z, // damagePosX + 0 // damageType ( not used ) + ) } -void function RunATGame_Threaded() +///////////////////////////////// +///// SCORING FUNCTIONS END ///// +///////////////////////////////// + + + +////////////////////////////// +///// GAMELOOP FUNCTIONS ///// +////////////////////////////// + +void function AT_GameLoop_Threaded() { svGlobal.levelEnt.EndSignal( "GameStateChanged" ) - OnThreadEnd( function() - { - SetGlobalNetBool( "banksOpen", false ) - }) + // game end func + // TODO: Cant seem to be able to get this crash ??? + OnThreadEnd + ( + function() + { + // prevent crash before entity creation on map change + if ( GetGameState() >= eGameState.Prematch ) + { + SetGlobalNetBool( "preBankPhase", false ) + SetGlobalNetBool( "banksOpen", false ) + } + } + ) - wait WAVE_STATE_TRANSITION_TIME // initial wait before first wave - - for ( int waveCount = 1; ; waveCount++ ) + // Initial wait before first wave + wait AT_FIRST_WAVE_START_DELAY - AT_WAVE_TRANSITION_DELAY + + int lastWaveId = -1 + for ( int waveCount = 1; ; waveCount++ ) { - wait WAVE_STATE_TRANSITION_TIME + wait AT_WAVE_TRANSITION_DELAY // cap to number of real waves - int waveId = ( waveCount / 2 ) - // last wave is clearly unfinished so don't use, just cap to last actually used one - if ( waveId >= GetWaveDataSize() - 1 ) + int waveId = ( waveCount - 1 ) / 2 + int waveCapAmount = 2 + waveId = int( min( waveId, GetWaveDataSize() - waveCapAmount ) ) + + // New wave dialogue + bool waveChanged = lastWaveId != waveId + if ( waveChanged ) + { + PlayFactionDialogueToTeam( "bh_newWave", TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_newWave", TEAM_MILITIA ) + } + else // same wave, second half { - waveId = GetWaveDataSize() - 2 - waveCount = waveId * 2 + PlayFactionDialogueToTeam( "bh_incoming", TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_incoming", TEAM_MILITIA ) } + + lastWaveId = waveId SetGlobalNetInt( "AT_currentWave", waveId ) - bool isBossWave = waveCount / float( 2 ) > waveId // odd number waveCount means boss wave + bool isBossWave = waveCount % 2 == 0 // even number waveCount means boss wave // announce the wave foreach ( entity player in GetPlayerArray() ) { if ( isBossWave ) + { Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnounceBoss" ) + } else - Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnouncePreParty", 0.0, waveId ) + { + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_AnnouncePreParty", + 0.0, // endTime ( not used ) + waveId // waveNum + ) + } } - wait WAVE_STATE_TRANSITION_TIME + wait AT_WAVE_TRANSITION_DELAY - // run the wave + // Run the wave + thread AT_CampSpawnThink( waveId, isBossWave ) + + if ( !isBossWave ) + { + svGlobal.levelEnt.WaitSignal( "ATAllCampsClean" ) // signaled when all camps cleaned in spawn functions + } + else + { + wait AT_BOUNTY_TITAN_CHECK_DELAY + // wait until all bounty titans killed + while ( IsAlive( GetGlobalNetEnt( "camp1Ent" ) ) || IsAlive( GetGlobalNetEnt( "camp2Ent" ) ) ) + WaitFrame() + } + + // wave end, prebank phase + svGlobal.levelEnt.Signal( "ATWaveEnd" ) // defensive fix, destroy existing campEnts + SetGlobalNetBool( "preBankPhase", true ) + + wait AT_WAVE_END_ANNOUNCEMENT_DELAY - AT_WaveData wave = GetWaveData( waveId ) - array< array<AT_SpawnData> > campSpawnData + // announce wave end + foreach ( entity player in GetPlayerArray() ) + { + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_AnnounceWaveOver", + waveId, // waveNum ( not used ) + 0, // militiaDamageTotal ( not used ) + 0, // imcDamageTotal ( not used ) + 0, // milMVP ( not used ) + 0, // imcMVP ( not used ) + 0, // milMVPDamage ( not used ) + 0 // imcMVPDamage ( not used ) + ) + } + + wait AT_WAVE_TRANSITION_DELAY - if ( isBossWave ) - campSpawnData = wave.bossSpawnData - else - campSpawnData = wave.spawnDataArrays + // banking phase + SetGlobalNetBool( "preBankPhase", false ) + SetGlobalNetTime( "AT_bankStartTime", Time() ) + SetGlobalNetTime( "AT_bankEndTime", Time() + AT_BANKS_OPEN_DURATION ) + SetGlobalNetBool( "banksOpen", true ) + + foreach ( entity player in GetPlayerArray() ) + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_BankOpen" ) + + foreach ( entity bank in file.banks ) + thread AT_BankActiveThink( bank ) + - // initialise pending spawns - foreach ( array< AT_SpawnData > campData in campSpawnData ) + float endTime = Time() + AT_BANKS_OPEN_DURATION + bool forceCloseTriggered = false + // wait until no player is holding bonus, or max wait time + while ( Time() <= endTime ) { - foreach ( AT_SpawnData spawnData in campData ) - spawnData.pendingSpawns = spawnData.totalToSpawn + // If everyone has deposited their bonuses close the banks early + if ( !ATAnyPlayerHasBonus() && !forceCloseTriggered ) + { + forceCloseTriggered = true + endTime = Time() + AT_BANK_FORCE_CLOSE_DELAY + } + + WaitFrame() } - // clear tracked spawns - file.trackedCampNPCSpawns = {} - while ( true ) - { - // if this is ever 0 by the end of this loop, wave is complete - int numActiveCampSpawners = 0 + SetGlobalNetBool( "banksOpen", false ) + foreach ( entity player in GetPlayerArray() ) + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_BankClose" ) + } +} + +bool function ATAnyPlayerHasBonus() +{ + foreach ( entity player in GetPlayerArray() ) + { + if ( AT_GetPlayerBonusPoints( player ) ) + return true + } + return false +} + +////////////////////////////////// +///// GAMELOOP FUNCTIONS END ///// +////////////////////////////////// + + + +////////////////////////// +///// CAMP FUNCTIONS ///// +////////////////////////// + +void function AT_CampSpawnThink( int waveId, bool isBossWave ) +{ + AT_WaveData wave = GetWaveData( waveId ) + array< array<AT_SpawnData> > campSpawnData + + if ( isBossWave ) + campSpawnData = wave.bossSpawnData + else + campSpawnData = wave.spawnDataArrays + + array<AT_WaveOrigin> allCampsToUse + foreach ( AT_WaveOrigin campStruct in file.camps ) + { + if ( campStruct.phaseAllowed[ waveId ] ) + allCampsToUse.append( campStruct ) + } + + // HACK + // There's too many phase3 camps on exoplanet and rise, make sure we always have the correct count + int maxCampsForWave = waveId == 0 ? 1 : 2 + while( allCampsToUse.len() > maxCampsForWave ) + { + // Get the required number of camps + array<AT_WaveOrigin> tempCamps + for( int i = 0; i < maxCampsForWave; i++ ) + tempCamps.append( allCampsToUse[RandomInt( allCampsToUse.len() )] ) - // iterate over camp data for wave - for ( int campIdx = 0; campIdx < campSpawnData.len() && campIdx < file.camps.len(); campIdx++ ) + + // Check if they're intersecting, if they are, try again + bool intersecting = false + for( int i = 0; i < tempCamps.len(); i++ ) + { + AT_WaveOrigin campA = tempCamps[i] + for( int j = 0; j < tempCamps.len(); j++ ) { - if ( !( campIdx in file.trackedCampNPCSpawns ) ) - file.trackedCampNPCSpawns[ campIdx ] <- {} + // Don't compare the same two camps + if( j == i ) + continue + + AT_WaveOrigin campB = tempCamps[j] + + if( Distance( campA.origin, campB.origin ) < campA.radius + campB.radius ) + intersecting = true + } + } + + if( !intersecting ) + allCampsToUse = tempCamps + + // If we ever get really unlucky just wait a frame + WaitFrame() + } + + foreach ( int spawnId, AT_WaveOrigin curCampData in allCampsToUse ) + { + array<AT_SpawnData> curSpawnData = campSpawnData[ spawnId ] + + int totalNPCsToSpawn = 0 + // initialise pending spawns and get total npcs + foreach ( AT_SpawnData spawnData in curSpawnData ) + { + spawnData.pendingSpawns = spawnData.totalToSpawn + // add to network variables + string npcNetVar = GetNPCNetVarName( spawnData.aitype, spawnId ) + SetGlobalNetInt( npcNetVar, spawnData.totalToSpawn ) + + totalNPCsToSpawn += spawnData.totalToSpawn + } + + if ( !isBossWave ) + { + // camp Ent, boss wave will use boss themselves as campEnt + string campEntVarName = "camp" + string( spawnId + 1 ) + "Ent" + bool waveNotActive = GetGlobalNetBool( "preBankPhase" ) || GetGlobalNetBool( "banksOpen" ) + if ( !IsValid( GetGlobalNetEnt( campEntVarName ) ) && !waveNotActive ) + SetGlobalNetEnt( campEntVarName, CreateCampTracker( curCampData, spawnId ) ) - // iterate over ai spawn data for camp - foreach ( AT_SpawnData spawnData in campSpawnData[ campIdx ] ) + array<AT_SpawnData> minionSquadDatas + foreach ( AT_SpawnData data in curSpawnData ) + { + switch ( data.aitype ) { - if ( !( spawnData.aitype in file.trackedCampNPCSpawns[ campIdx ] ) ) - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] <- 0 - - if ( spawnData.pendingSpawns > 0 || file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] > 0 ) - numActiveCampSpawners++ + case "npc_soldier": + case "npc_spectre": + case "npc_stalker": + if ( !AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + minionSquadDatas.append( data ) + else + thread AT_DroppodSquadEvent_Single( curCampData, spawnId, data ) + break + + case "npc_super_spectre": + thread AT_ReaperEvent( curCampData, spawnId, data ) + break + } + } + + // minions squad spawn + if ( !AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + { + if ( minionSquadDatas.len() > 0 ) + thread AT_DroppodSquadEvent( curCampData, spawnId, minionSquadDatas ) + } + + // use campProgressThink for handling wave state + thread CampProgressThink( spawnId, totalNPCsToSpawn ) + } + else // bosswave spawn + { + foreach ( AT_SpawnData data in curSpawnData ) + { + if( data.aitype != "npc_titan" ) + continue - // try to spawn as many ai as we can, as long as the camp doesn't already have too many spawned - int spawnCount - for ( spawnCount = 0; spawnCount < spawnData.pendingSpawns && spawnCount < spawnData.totalAllowedOnField - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ]; ) - { - // not doing this in a generic way atm, but could be good for the future if we want to support more ai - switch ( spawnData.aitype ) - { - case "npc_soldier": - case "npc_spectre": - case "npc_stalker": - thread AT_SpawnDroppodSquad( campIdx, spawnData.aitype ) - spawnCount += 4 - break - - case "npc_super_spectre": - thread AT_SpawnReaper( campIdx ) - spawnCount += 1 - break - - case "npc_titan": - thread AT_SpawnBountyTitan( campIdx ) - spawnCount += 1 - break - - default: - print( "BOUNTY HUNT: Tried to spawn unsupported ai of type \"" + "\" at camp " + campIdx ) - } + thread AT_BountyTitanEvent( curCampData, spawnId, data ) + break + } + } + } +} + +void function CampProgressThink( int spawnId, int totalNPCsToSpawn ) +{ + string campLetter = GetCampLetter( spawnId ) + string campProgressName = campLetter + "campProgress" + string campEntVarName = "camp" + string( spawnId + 1 ) + "Ent" + + // initial wait + SetGlobalNetFloat( campProgressName, 1.0 ) + + // TODO: random wait, make this a constant ?? + wait 3.0 + + float cleanUpTime = -1.0 + + while ( true ) + { + int npcsLeft + // get all npcs might be in this camp + for ( int i = 0; i < 5; i++ ) + { + string netVarName = string( i + 1 ) + campLetter + "campCount" + int netVarValue = GetGlobalNetInt( netVarName ) + if ( netVarValue >= 0 ) // uninitialized network var starts from -1, avoid checking them + npcsLeft += netVarValue + } + + float campLeft = float( npcsLeft ) / float( totalNPCsToSpawn ) + SetGlobalNetFloat( campProgressName, campLeft ) + + if( npcsLeft <= AT_CAMP_BORED_NPCS_LEFT_TO_START_CLEANUP && cleanUpTime < 0.0 ) + { + cleanUpTime = Time() + AT_CAMP_BORED_CLEANUP_WAIT + print("Cleanup timer started!") + } + + if( Time() > cleanUpTime && cleanUpTime > 0.0 && spawnId in file.campScriptEntArrays ) + { + foreach( int handle in file.campScriptEntArrays[spawnId] ) + { + array<entity> entities = GetScriptManagedEntArray( handle ) + entities.removebyvalue( null ) + foreach ( entity ent in entities ) + { + if ( IsAlive( ent ) && ent.IsNPC() ) + { + printt( "Killing bored AI " + ent.GetClassName() + " at " + ent.GetOrigin() ) + ent.Die() } - - // track spawns - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] += spawnCount - spawnData.pendingSpawns -= spawnCount } } + } + + if ( campLeft <= 0.0 ) // camp wiped! + { + PlayFactionDialogueToTeam( "bh_cleared" + campLetter, TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_cleared" + campLetter, TEAM_MILITIA ) + + entity campEnt = GetGlobalNetEnt( campEntVarName ) + if ( IsValid( campEnt ) ) + campEnt.Signal( "ATCampClean" ) // destroy the camp ent + + // check if both camps being destroyed + if ( !IsValid( GetGlobalNetEnt( "camp1Ent" ) ) && !IsValid( GetGlobalNetEnt( "camp2Ent" ) ) ) + svGlobal.levelEnt.Signal( "ATAllCampsClean" ) // end the wave - if ( numActiveCampSpawners == 0 ) - break - - wait 0.5 + return } - - wait WAVE_STATE_TRANSITION_TIME - - // banking phase + + WaitFrame() } } // entity funcs +// camp +entity function CreateCampTracker( AT_WaveOrigin campData, int spawnId ) +{ + // store data + vector campOrigin = campData.origin + float campRadius = campData.radius + float campHeight = campData.height + // add a minimap icon + entity mapIconEnt = CreateEntity( "prop_script" ) + DispatchSpawn( mapIconEnt ) + + mapIconEnt.SetOrigin( campOrigin ) + mapIconEnt.DisableHibernation() + SetTeam( mapIconEnt, AT_AI_TEAM ) + mapIconEnt.Minimap_AlwaysShow( TEAM_IMC, null ) + mapIconEnt.Minimap_AlwaysShow( TEAM_MILITIA, null ) + + mapIconEnt.Minimap_SetCustomState( GetCampMinimapState( spawnId ) ) + mapIconEnt.Minimap_SetAlignUpright( true ) + mapIconEnt.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + mapIconEnt.Minimap_SetObjectScale( campRadius / 16000.0 ) // proper icon on the map + + // attach a location tracker + entity tracker = GetAvailableLocationTracker() + tracker.SetOwner( mapIconEnt ) // needs a owner to show up + tracker.SetOrigin( campOrigin ) + SetLocationTrackerRadius( tracker, campRadius ) + SetLocationTrackerID( tracker, spawnId ) + DispatchSpawn( tracker ) + + thread TrackWaveEndForCampInfo( tracker, mapIconEnt ) + return tracker +} + +void function TrackWaveEndForCampInfo( entity tracker, entity mapIconEnt ) +{ + tracker.EndSignal( "OnDestroy" ) + tracker.EndSignal( "ATCampClean" ) + + OnThreadEnd + ( + function(): ( tracker, mapIconEnt ) + { + // camp cleaned, wave or game ended, destroy the camp info + if ( IsValid( tracker ) ) + tracker.Destroy() + + if ( IsValid( mapIconEnt ) ) + mapIconEnt.Destroy() + } + ) + + WaitSignal( svGlobal.levelEnt, "GameStateChanged", "ATWaveEnd" ) +} + +string function GetCampLetter( int spawnId ) +{ + return spawnId == 0 ? "A" : "B" +} + +int function GetCampMinimapState( int id ) +{ + switch ( id ) + { + case 0: + return eMinimapObject_prop_script.AT_DROPZONE_A + case 1: + return eMinimapObject_prop_script.AT_DROPZONE_B + case 2: + return eMinimapObject_prop_script.AT_DROPZONE_C + } + + unreachable +} + +////////////////////////////// +///// CAMP FUNCTIONS END ///// +////////////////////////////// + + + +////////////////////////// +///// BANK FUNCTIONS ///// +////////////////////////// + +void function AT_BankActiveThink( entity bank ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + bank.EndSignal( "OnDestroy" ) + + // Banks closed + OnThreadEnd + ( + function(): ( bank ) + { + if ( IsValid( bank ) ) + { + // Update use prompt + if ( GetGameState() != eGameState.Playing ) + bank.UnsetUsable() + else + bank.SetUsePrompts( "#AT_USE_BANK_CLOSED", "#AT_USE_BANK_CLOSED" ) + + thread PlayAnim( bank, "mh_active_2_inactive" ) + FadeOutSoundOnEntity( bank, "Mobile_Hardpoint_Idle", 0.5 ) + bank.Minimap_Hide( TEAM_IMC, null ) + bank.Minimap_Hide( TEAM_MILITIA, null ) + } + } + ) + + // Update use prompt to usable + bank.SetUsable() + bank.SetUsePrompts( "#AT_USE_BANK", "#AT_USE_BANK_PC" ) + + thread PlayAnim( bank, "mh_inactive_2_active" ) + EmitSoundOnEntity( bank, "Mobile_Hardpoint_Idle" ) + + // Show minimap icon for bank + bank.Minimap_AlwaysShow( TEAM_IMC, null ) + bank.Minimap_AlwaysShow( TEAM_MILITIA, null ) + bank.Minimap_SetCustomState( eMinimapObject_prop_script.AT_BANK ) + + // Wait for bank close or game end + while ( GetGlobalNetBool( "banksOpen" ) ) + WaitFrame() +} + +function OnPlayerUseBank( bank, player ) +{ + // Banks are always usable so that we can show the use prompt + // Only allow deposit when banks are open + if ( !GetGlobalNetBool( "banksOpen" ) ) + return + + expect entity( bank ) + expect entity( player ) + + // bank.SetUsableByGroup( "pilot" ) didn't seem to work so we just + // exit here if player is in a titan + if( player.IsTitan() ) + return + + // Player has no bonus, try to send a tip using SendHUDMessage + if ( AT_GetPlayerBonusPoints( player ) == 0 ) + { + ATSendDepositTipToPlayer( player, "#AT_USE_BANK_NO_BONUS_HINT" ) + return + } + + // Prevent more than one instance of this thread running + if ( !file.playerBankUploading[ player ] ) + thread PlayerUploadingBonus_Threaded( bank, player ) +} + +bool function ATSendDepositTipToPlayer( entity player, string message ) +{ + if ( Time() < file.playerHudMessageAllowedTime[ player ] ) + return false + + SendHudMessage( player, message, -1, 0.4, 255, 255, 255, 255, 0.5, 1.0, 0.5 ) + file.playerHudMessageAllowedTime[ player ] = Time() + AT_PLAYER_HUD_MESSAGE_COOLDOWN + + return true +} + +struct AT_playerUploadStruct +{ + bool uploadSuccess = false + int depositedPoints = 0 +} + +void function PlayerUploadingBonus_Threaded( entity bank, entity player ) +{ + bank.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) + + file.playerBankUploading[ player ] = true + + // this literally only exists because structs are passed by ref, + // and primitives like ints and bools are passed by val + // which meant that the OnThreadEnd was just getting 0 and false + AT_playerUploadStruct uploadInfo + + // Cleanup and call finish deposit func + OnThreadEnd + ( + function(): ( player, uploadInfo ) + { + if ( IsValid( player ) ) + { + file.playerBankUploading[ player ] = false + + // Clean up looping sound + StopSoundOnEntity( player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_1P" ) + StopSoundOnEntity( player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_3P" ) + + // Do medal event + // TODO: Check if vanilla actually do.s this every time you finish depositing??? + AddPlayerScore( player, "AttritionCashedBonus" ) + + // Do server callback + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_FinishDeposit", + uploadInfo.depositedPoints // deposit + ) + + player.SetPlayerNetBool( "AT_playerUploading", false ) + + if ( uploadInfo.uploadSuccess ) // Player deposited all remaining bonus + { + // Emit uploading successful sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Successful_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Successful_3P" ) + + // player is MVP + int ourScore = player.GetPlayerGameStat( PGS_ASSAULT_SCORE ) + bool isMVP = true + foreach(teamPlayer in GetPlayerArrayOfTeam(player.GetTeam())) + { + if (ourScore < teamPlayer.GetPlayerGameStat( PGS_ASSAULT_SCORE )) + { + isMVP = false + break + } + } + if (isMVP) + PlayFactionDialogueToPlayer( "bh_mvp", player ) + } + else // Player was killed or left the bank radius + { + // Emit uploading failed sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Unsuccessful_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Unsuccessful_3P" ) + } + } + } + ) + + // Uploading start sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_Start_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_Start_3P" ) + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_3P" ) + + player.SetPlayerNetBool( "AT_playerUploading", true ) + + // Upload bonus while the player is within range of the bank + while ( Distance( player.GetOrigin(), bank.GetOrigin() ) <= AT_BANK_DEPOSIT_RADIUS && GetGlobalNetBool( "banksOpen" ) ) + { + // Calling this moves the "Uploading..." graphic to the same place it is + // in vanilla + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_ShowRespawnBonusLoss" ) + + int bonusToUpload = int( min( AT_BANK_DEPOSIT_RATE, AT_GetPlayerBonusPoints( player ) ) ) + // No more bonus to upload, return + if ( bonusToUpload == 0 ) + { + uploadInfo.uploadSuccess = true + return + } + + // Remove bonus points and add them to total poins + AT_AddPlayerBonusPoints( player, -bonusToUpload ) + AT_AddPlayerTotalPoints( player, bonusToUpload ) + + uploadInfo.depositedPoints += bonusToUpload + WaitFrame() + } +} + +////////////////////////////// +///// BANK FUNCTIONS END ///// +////////////////////////////// -void function AT_SpawnDroppodSquad( int camp, string aiType ) + + +///////////////////////// +///// NPC FUNCTIONS ///// +///////////////////////// + +int function GetScriptManagedNPCArrayLength_Alive( int scriptManagerId ) +{ + array<entity> entities = GetScriptManagedEntArray( scriptManagerId ) + entities.removebyvalue( null ) + int npcsAlive = 0 + foreach ( entity ent in entities ) + { + if ( IsAlive( ent ) && ent.IsNPC() ) + npcsAlive += 1 + } + return npcsAlive +} + +void function AT_DroppodSquadEvent( AT_WaveOrigin campData, int spawnId, array<AT_SpawnData> minionDatas ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + // create a script managed array for all handled minions + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = SQUAD_SIZE * AT_DROPPOD_SQUADS_ALLOWED_ON_FIELD + while ( true ) + { + foreach ( AT_SpawnData data in minionDatas ) + { + string ent = data.aitype + waitthread AT_SpawnDroppodSquad( campData, spawnId, ent, eventManager ) + data.pendingSpawns -= SQUAD_SIZE + if ( data.pendingSpawns <= 0 ) // current spawn data has reached max spawn amount + minionDatas.removebyvalue( data ) // remove this data + if ( GetScriptManagedNPCArrayLength_Alive( eventManager ) >= totalAllowedOnField ) // we have enough npcs on field? + break // stop following spawning functions + } + if ( minionDatas.len() == 0 ) // all spawn data has finished spawn + return + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField - SQUAD_SIZE ) // wait until we have lost more than 1 squad + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } +} + +// for AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK, handles a single spawndata +void function AT_DroppodSquadEvent_Single( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // get ent and create a script managed array for current event + string ent = data.aitype + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = data.totalAllowedOnField // mostly 12 for grunts and spectres, too much! + // start spawner + while ( true ) + { + waitthread AT_SpawnDroppodSquad( campData, spawnId, ent, eventManager ) + data.pendingSpawns -= SQUAD_SIZE + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField - SQUAD_SIZE ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } +} + +void function AT_SpawnDroppodSquad( AT_WaveOrigin campData, int spawnId, string aiType, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.dropPodSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].dropPodSpawnPoints.getrandom() + spawnpoint = campData.dropPodSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) - AiGameModes_SpawnDropPod( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, aiType, void function( array<entity> guys ) : ( camp, aiType ) - { - AT_HandleSquadSpawn( guys, camp, aiType ) - }) + AiGameModes_SpawnDropPod( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + aiType, + // squad handler + void function( array<entity> guys ) : ( campData, spawnId, aiType, scriptManagerId ) + { + AT_HandleSquadSpawn( guys, campData, spawnId, aiType, scriptManagerId ) + }, + eDropPodFlag.DISSOLVE_AFTER_DISEMBARKS + ) } -void function AT_HandleSquadSpawn( array<entity> guys, int camp, string aiType ) +void function AT_HandleSquadSpawn( array<entity> guys, AT_WaveOrigin campData, int spawnId, string aiType, int scriptManagerId ) { foreach ( entity guy in guys ) { - guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) - - // untrack them on death - thread AT_WaitToUntrackNPC( guy, camp, aiType ) + // TODO: NPCs still seem to go outside their camp ??? + //guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) + + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, guy ) + thread AT_TrackNPCLifeTime( guy, spawnId, aiType ) + + thread AT_ForceAssaultAroundCamp( guy, campData ) + } +} + +void function AT_ForceAssaultAroundCamp( entity guy, AT_WaveOrigin campData ) +{ + guy.EndSignal( "OnDestroy" ) + guy.EndSignal( "OnDeath" ) + + // goal check + vector ornull goalPos = NavMesh_ClampPointForAI(campData.origin, guy) + goalPos = goalPos == null ? campData.origin : goalPos + expect vector(goalPos) + + float goalRadius = campData.radius / 4 + float guyGoalRadius = guy.GetMinGoalRadius() + if ( guyGoalRadius > goalRadius ) // this npc cannot use forced goal radius? + goalRadius = guyGoalRadius + + while( true ) + { + guy.AssaultPoint( goalPos ) + guy.AssaultSetGoalRadius( goalRadius ) + guy.AssaultSetFightRadius( 0 ) + guy.AssaultSetArrivalTolerance( int(goalRadius) ) + + wait RandomFloatRange( 1, 5 ) + } +} + +void function AT_ReaperEvent( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // create a script managed array for current event + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = 1 // 1 allowed at the same time for heavy armor units + if ( AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + totalAllowedOnField = data.totalAllowedOnField + + while ( true ) + { + waitthread AT_SpawnReaper( campData, spawnId, eventManager ) + data.pendingSpawns -= 1 + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } } } -void function AT_SpawnReaper( int camp ) +void function AT_SpawnReaper( AT_WaveOrigin campData, int spawnId, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.dropPodSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + spawnpoint = campData.dropPodSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) - AiGameModes_SpawnReaper( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, "npc_super_spectre",void function( entity reaper ) : ( camp ) + AiGameModes_SpawnReaper( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + "npc_super_spectre_aitdm", + // reaper handler + void function( entity reaper ) : ( campData, spawnId, scriptManagerId ) + { + AT_HandleReaperSpawn( reaper, campData, spawnId, scriptManagerId ) + } + ) +} + +void function AT_HandleReaperSpawn( entity reaper, AT_WaveOrigin campData, int spawnId, int scriptManagerId ) +{ + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, reaper ) + thread AT_TrackNPCLifeTime( reaper, spawnId, "npc_super_spectre" ) + + thread AT_ForceAssaultAroundCamp( reaper, campData ) +} + +void function AT_BountyTitanEvent( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // create a script managed array for current event + int eventManager = CreateScriptManagedEntArray() + + int totalAllowedOnField = 1 // 1 allowed at the same time for heavy armor units + if ( AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + totalAllowedOnField = data.totalAllowedOnField + while ( true ) { - thread AT_WaitToUntrackNPC( reaper, camp, "npc_super_spectre" ) - }) + waitthread AT_SpawnBountyTitan( campData, spawnId, eventManager ) + data.pendingSpawns -= 1 + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } } -void function AT_SpawnBountyTitan( int camp ) +void function AT_SpawnBountyTitan( AT_WaveOrigin campData, int spawnId, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.titanSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + spawnpoint = campData.titanSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) @@ -320,57 +1630,178 @@ void function AT_SpawnBountyTitan( int camp ) int bountyID = 0 try { - bountyID = ReserveBossID( VALID_BOUNTY_TITAN_SETTINGS.getrandom() ) + bountyID = ReserveBossID( AT_BOUNTY_TITANS_AI_SETTINGS.getrandom() ) } catch ( ex ) {} // if we go above the expected wave count that vanilla supports, there's basically no way to ensure that this func won't error, so default 0 after that point string aisettings = GetTypeFromBossID( bountyID ) string titanClass = expect string( Dev_GetAISettingByKeyField_Global( aisettings, "npc_titan_player_settings" ) ) + AiGameModes_SpawnTitan( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + titanClass, + aisettings, + // titan handler + void function( entity titan ) : ( campData, spawnId, bountyID, scriptManagerId ) + { + AT_HandleBossTitanSpawn( titan, campData, spawnId, bountyID, scriptManagerId ) + } + ) +} + +void function AT_HandleBossTitanSpawn( entity titan, AT_WaveOrigin campData, int spawnId, int bountyID, int scriptManagerId ) +{ + // set the bounty to be campEnt, for client tracking + SetGlobalNetEnt( "camp" + string( spawnId + 1 ) + "Ent", titan ) + // set up health + titan.SetMaxHealth( titan.GetMaxHealth() * AT_BOUNTY_TITAN_HEALTH_MULTIPLIER ) + titan.SetHealth( titan.GetMaxHealth() ) + // make minimap always show them and highlight them + titan.Minimap_AlwaysShow( TEAM_IMC, null ) + titan.Minimap_AlwaysShow( TEAM_MILITIA, null ) + thread BountyBossHighlightThink( titan ) + + // set up titan-specific death callbacks, mark it as bounty boss + file.titanIsBountyBoss[ titan ] <- true + file.bountyTitanRewards[ titan ] <- ATTRITION_SCORE_BOSS_DAMAGE + AddEntityCallback_OnPostDamaged( titan, OnBountyTitanPostDamage ) + AddEntityCallback_OnKilled( titan, OnBountyTitanKilled ) - AiGameModes_SpawnTitan( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, titanClass, aisettings, void function( entity titan ) : ( camp, bountyID ) + titan.GetTitanSoul().soul.skipDoomState = true + // i feel like this should be localised, but there's nothing for it in r1_english? + titan.SetTitle( GetNameFromBossID( bountyID ) ) + + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, titan ) + thread AT_TrackNPCLifeTime( titan, spawnId, "npc_titan" ) +} + +void function BountyBossHighlightThink( entity titan ) +{ + titan.EndSignal( "OnDestroy" ) + titan.EndSignal( "OnDeath" ) + + while ( true ) { - // set up titan-specific death/damage callbacks - AddEntityCallback_OnDamaged( titan, OnBountyDamaged) - AddEntityCallback_OnKilled( titan, OnBountyKilled ) - - titan.GetTitanSoul().soul.skipDoomState = true - // i feel like this should be localised, but there's nothing for it in r1_english? - titan.SetTitle( GetNameFromBossID( bountyID ) ) - thread AT_WaitToUntrackNPC( titan, camp, "npc_titan" ) - } ) + Highlight_SetEnemyHighlight( titan, "enemy_boss_bounty" ) + titan.WaitSignal( "StopPhaseShift" ) // prevent phase shift mess up highlights + } } -// Tracked entities will require their own "wallet" -// for titans it should be used for rounding error compenstation -// for infantry it sould be used to store money if the npc kills a player -void function OnBountyDamaged( entity titan, var damageInfo ) +void function OnBountyTitanPostDamage( entity titan, var damageInfo ) { entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !IsValid( attacker ) ) // delayed by projectile shots + return + // damaged by npc or something? if ( !attacker.IsPlayer() ) - attacker = GetLatestAssistingPlayerInfo( titan ).player - - if ( IsValid( attacker ) && attacker.IsPlayer() ) { - int reward = int ( BOUNTY_TITAN_DAMAGE_POOL * DamageInfo_GetDamage( damageInfo ) / titan.GetMaxHealth() ) - printt ( titan.GetMaxHealth(), DamageInfo_GetDamage( damageInfo ) ) - - AT_AddPlayerCash( attacker, reward ) + attacker = GetBountyBossDamageOwner( attacker, titan ) + if ( !IsValid( attacker ) || !attacker.IsPlayer() ) + return } + + int rewardSegment = ATTRITION_SCORE_BOSS_DAMAGE + int healthSegment = titan.GetMaxHealth() / rewardSegment + + // sometimes damage is not enough to add 1 point, we save the damage for player's next attack + if ( !( titan in file.playerSavedBountyDamage[ attacker ] ) ) + file.playerSavedBountyDamage[ attacker ][ titan ] <- 0 + + file.playerSavedBountyDamage[ attacker ][ titan ] += int( DamageInfo_GetDamage( damageInfo ) ) + if ( file.playerSavedBountyDamage[ attacker ][ titan ] < healthSegment ) + return // they can't earn reward from this shot + + int damageSegment = file.playerSavedBountyDamage[ attacker ][ titan ] / healthSegment + int savedDamageLeft = file.playerSavedBountyDamage[ attacker ][ titan ] % healthSegment + file.playerSavedBountyDamage[ attacker ][ titan ] = savedDamageLeft + + float damageFrac = float( damageSegment ) / rewardSegment + int rewardLeft = file.bountyTitanRewards[ titan ] + int reward = int( ATTRITION_SCORE_BOSS_DAMAGE * damageFrac ) + if ( reward >= rewardLeft ) // overloaded shot? + reward = rewardLeft + file.bountyTitanRewards[ titan ] -= reward + + if ( reward > 0 ) + AT_AddPlayerBonusPointsForBossDamaged( attacker, titan, reward, damageInfo ) } -void function OnBountyKilled( entity titan, var damageInfo ) +void function OnBountyTitanKilled( entity titan, var damageInfo ) { entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !IsValid( attacker ) ) // delayed by projectile shots + return + // damaged by npc or something? if ( !attacker.IsPlayer() ) - attacker = GetLatestAssistingPlayerInfo( titan ).player + { + attacker = GetBountyBossDamageOwner( attacker, titan ) + if ( !IsValid( attacker ) || !attacker.IsPlayer() ) + return + } + + // add all remaining reward to attacker + // bounty killed bonus handled by AT_PlayerOrNPCKilledScoreEvent() + int rewardLeft = file.bountyTitanRewards[ titan ] + delete file.bountyTitanRewards[ titan ] + if ( rewardLeft > 0 ) + AT_AddPlayerBonusPointsForBossDamaged( attacker, titan, rewardLeft, damageInfo ) + + // remove this bounty's damage saver + foreach ( entity player in GetPlayerArray() ) + { + if ( titan in file.playerSavedBountyDamage[ player ] ) + delete file.playerSavedBountyDamage[ player ][ titan ] + } + + // faction dialogue + int team = attacker.GetTeam() + PlayFactionDialogueToPlayer( "bh_playerKilledBounty", attacker ) + PlayFactionDialogueToTeamExceptPlayer( "bh_bountyClaimedByFriendly", team, attacker ) + PlayFactionDialogueToTeam( "bh_bountyClaimedByEnemy", GetOtherTeam( team ) ) +} + +entity function GetBountyBossDamageOwner( entity attacker, entity titan ) +{ + if ( attacker.IsPlayer() ) // already a player + return attacker - if ( IsValid( attacker ) && attacker.IsPlayer() ) - AT_AddPlayerCash( attacker, BOUNTY_TITAN_KILL_REWARD ) + if ( attacker.IsTitan() ) // attacker is a npc titan + { + // try to find it's pet titan owner + if ( IsValid( GetPetTitanOwner( attacker ) ) ) + return GetPetTitanOwner( attacker ) + } + + // other damages or non-owner npcs, not sure how it happens, just use this titan's last attacker + return GetLatestAssistingPlayerInfo( titan ).player } -void function AT_WaitToUntrackNPC( entity guy, int camp, string aiType ) +void function AT_TrackNPCLifeTime( entity guy, int spawnId, string aiType ) { guy.WaitSignal( "OnDeath", "OnDestroy" ) - file.trackedCampNPCSpawns[ camp ][ aiType ]-- + + string npcNetVar = GetNPCNetVarName( aiType, spawnId ) + SetGlobalNetInt( npcNetVar, GetGlobalNetInt( npcNetVar ) - 1 ) +} + + +// network var +string function GetNPCNetVarName( string className, int spawnId ) +{ + string npcId = string( GetAiTypeInt( className ) + 1 ) + string campLetter = GetCampLetter( spawnId ) + if ( npcId == "0" ) // cannot find this ai support! + { + if ( className == "npc_super_spectre" ) // stupid, reapers are not handled by GetAiTypeInt(), but it must be 4 + return "4" + campLetter + "campCount" + return "" + } + return npcId + campLetter + "campCount" } + +///////////////////////////// +///// NPC FUNCTIONS END ///// +///////////////////////////// diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut index 705b7836..d8b0c9bd 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut @@ -29,6 +29,8 @@ struct { array<HardpointStruct> hardpoints array<CP_PlayerStruct> players + table<entity,int> playerAssaultPoints + table<entity,int> playerDefensePoints } file void function GamemodeCP_Init() @@ -112,11 +114,13 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama { AddPlayerScore( attacker , "HardpointDefense", victim ) attacker.AddToPlayerGameStat(PGS_DEFENSE_SCORE,POINTVALUE_HARDPOINT_DEFENSE) + UpdatePlayerScoreForChallenge(attacker,0,POINTVALUE_HARDPOINT_DEFENSE) } else if((victimCP.hardpoint.GetTeam()==victim.GetTeam())||(GetHardpointCappingTeam(victimCP)==victim.GetTeam())) { AddPlayerScore( attacker, "HardpointAssault", victim ) attacker.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_ASSAULT) + UpdatePlayerScoreForChallenge(attacker,POINTVALUE_HARDPOINT_ASSAULT,0) } } } @@ -127,10 +131,12 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama { AddPlayerScore( attacker , "HardpointSnipe", victim ) attacker.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_SNIPE) + UpdatePlayerScoreForChallenge(attacker,POINTVALUE_HARDPOINT_SNIPE,0) } else{ AddPlayerScore( attacker , "HardpointSiege", victim ) attacker.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_SIEGE) + UpdatePlayerScoreForChallenge(attacker,POINTVALUE_HARDPOINT_SIEGE,0) } } else if(attackerCP.hardpoint!=null)//Perimeter Defense @@ -138,6 +144,7 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama if(attackerCP.hardpoint.GetTeam()==attacker.GetTeam()) AddPlayerScore( attacker , "HardpointPerimeterDefense", victim) attacker.AddToPlayerGameStat(PGS_DEFENSE_SCORE,POINTVALUE_HARDPOINT_PERIMETER_DEFENSE) + UpdatePlayerScoreForChallenge(attacker,0,POINTVALUE_HARDPOINT_PERIMETER_DEFENSE) } foreach(CP_PlayerStruct player in file.players) //Reset Victim Holdtime Counter @@ -308,6 +315,7 @@ void function CapturePointForTeam(HardpointStruct hardpoint, int Team) if(player.IsPlayer()){ AddPlayerScore(player,"ControlPointCapture") player.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_CAPTURE) + UpdatePlayerScoreForChallenge(player,POINTVALUE_HARDPOINT_CAPTURE,0) } } } @@ -319,12 +327,17 @@ void function GamemodeCP_InitPlayer(entity player) playerStruct.timeOnPoints = [0.0,0.0,0.0] playerStruct.isOnHardpoint = false file.players.append(playerStruct) + file.playerAssaultPoints[player] <- 0 + file.playerDefensePoints[player] <- 0 thread PlayerThink(playerStruct) } void function GamemodeCP_RemovePlayer(entity player) { - + if(player in file.playerAssaultPoints) + delete file.playerAssaultPoints[player] + if(player in file.playerDefensePoints) + delete file.playerDefensePoints[player] foreach(index,CP_PlayerStruct playerStruct in file.players) { if(playerStruct.player==player) @@ -376,11 +389,13 @@ void function PlayerThink(CP_PlayerStruct player) { AddPlayerScore(player.player,"ControlPointAmpedHold") player.player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, POINTVALUE_HARDPOINT_AMPED_HOLD ) + UpdatePlayerScoreForChallenge(player.player,0,POINTVALUE_HARDPOINT_AMPED_HOLD) } else { AddPlayerScore(player.player,"ControlPointHold") player.player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, POINTVALUE_HARDPOINT_HOLD ) + UpdatePlayerScoreForChallenge(player.player,0,POINTVALUE_HARDPOINT_HOLD) } } break @@ -471,8 +486,10 @@ void function HardpointThink( HardpointStruct hardpoint ) } else if(cappingTeam==TEAM_UNASSIGNED) // nobody on point { - if((GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_AMPED)||(GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_AMPING)) + if((GetHardpointState(hardpoint)>=CAPTURE_POINT_STATE_AMPED) || (GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_SELF_UNAMPING)) { + if (GetHardpointState(hardpoint) == CAPTURE_POINT_STATE_AMPED) + SetHardpointState(hardpoint,CAPTURE_POINT_STATE_SELF_UNAMPING) // plays a pulsating effect on the UI only when the hardpoint is amped SetHardpointCappingTeam(hardpoint,hardpointEnt.GetTeam()) SetHardpointCaptureProgress(hardpoint,max(1.0,GetHardpointCaptureProgress(hardpoint)-(deltaTime/HARDPOINT_AMPED_DELAY))) if(GetHardpointCaptureProgress(hardpoint)<=1.001) // unamp @@ -546,8 +563,10 @@ void function HardpointThink( HardpointStruct hardpoint ) } else if(file.ampingEnabled)//amping or reamping { - if(GetHardpointState(hardpoint)<CAPTURE_POINT_STATE_AMPING) - SetHardpointState(hardpoint,CAPTURE_POINT_STATE_AMPING) + // i have no idea why but putting it CAPTURE_POINT_STATE_AMPING will say 'CONTESTED' on the UI + // since whether the point is contested is checked above, putting the hardpoint state to a value of 8 fixes it somehow + if(GetHardpointState(hardpoint)<=CAPTURE_POINT_STATE_AMPING) + SetHardpointState( hardpoint, 8 ) SetHardpointCaptureProgress( hardpoint, min( 2.0, GetHardpointCaptureProgress( hardpoint ) + ( deltaTime / HARDPOINT_AMPED_DELAY * capperAmount ) ) ) if(GetHardpointCaptureProgress(hardpoint)==2.0&&!(GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_AMPED)) { @@ -570,6 +589,7 @@ void function HardpointThink( HardpointStruct hardpoint ) { AddPlayerScore(player,"ControlPointAmped") player.AddToPlayerGameStat(PGS_DEFENSE_SCORE,POINTVALUE_HARDPOINT_AMPED) + UpdatePlayerScoreForChallenge(player,0,POINTVALUE_HARDPOINT_AMPED) } } } @@ -645,7 +665,10 @@ void function OnHardpointEntered( entity trigger, entity player ) hardpoint.militiaCappers.append( player ) foreach(CP_PlayerStruct playerStruct in file.players) if(playerStruct.player == player) + { playerStruct.isOnHardpoint = true + player.SetPlayerNetInt( "playerHardpointID", hardpoint.hardpoint.GetHardpointID() ) + } } void function OnHardpointLeft( entity trigger, entity player ) @@ -661,7 +684,10 @@ void function OnHardpointLeft( entity trigger, entity player ) FindAndRemove( hardpoint.militiaCappers, player ) foreach(CP_PlayerStruct playerStruct in file.players) if(playerStruct.player == player) + { playerStruct.isOnHardpoint = false + player.SetPlayerNetInt( "playerHardpointID", 69 ) // an arbitary number to remove the hud from the player + } } string function CaptureStateToString( int state ) @@ -675,6 +701,7 @@ string function CaptureStateToString( int state ) case CAPTURE_POINT_STATE_CAPTURED: return "CAPTURED" case CAPTURE_POINT_STATE_AMPING: + case 8: return "AMPING" case CAPTURE_POINT_STATE_AMPED: return "AMPED" @@ -703,3 +730,26 @@ string function GetHardpointGroup(entity hardpoint) //Hardpoint Entity B on Home return string(hardpoint.kv.hardpointGroup) } + +void function UpdatePlayerScoreForChallenge(entity player,int assaultpoints = 0,int defensepoints = 0) +{ + if(player in file.playerAssaultPoints) + { + file.playerAssaultPoints[player] += assaultpoints + if( file.playerAssaultPoints[player] >= 1000 && !HasPlayerCompletedMeritScore(player) ) + { + AddPlayerScore(player,"ChallengeCPAssault") + SetPlayerChallengeMeritScore(player) + } + } + + if(player in file.playerDefensePoints) + { + file.playerDefensePoints[player] += defensepoints + if( file.playerDefensePoints[player] >= 500 && !HasPlayerCompletedMeritScore(player) ) + { + AddPlayerScore(player,"ChallengeCPDefense") + SetPlayerChallengeMeritScore(player) + } + } +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut index 99f34164..97addc24 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut @@ -26,6 +26,8 @@ void function CaptureTheFlag_Init() { PrecacheModel( CTF_FLAG_MODEL ) PrecacheModel( CTF_FLAG_BASE_MODEL ) + PrecacheParticleSystem( FLAG_FX_FRIENDLY ) + PrecacheParticleSystem( FLAG_FX_ENEMY ) CaptureTheFlagShared_Init() SetSwitchSidesBased( true ) @@ -73,27 +75,20 @@ void function RateSpawnpoints_CTF( int checkClass, array<entity> spawnpoints, in bool function VerifyCTFSpawnpoint( entity spawnpoint, int team ) { // ensure spawnpoints aren't too close to enemy base + vector allyFlagSpot + vector enemyFlagSpot + foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) ) + { + if( spawn.GetTeam() == team ) + allyFlagSpot = spawn.GetOrigin() + else + enemyFlagSpot = spawn.GetOrigin() + } - if ( HasSwitchedSides() ) - team = GetOtherTeam( team ) - - array<entity> startSpawns = SpawnPoints_GetPilotStart( team ) - array<entity> enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( team ) ) - - vector averageFriendlySpawns - vector averageEnemySpawns - - foreach ( entity spawn in startSpawns ) - averageFriendlySpawns += spawn.GetOrigin() - - averageFriendlySpawns /= startSpawns.len() - - foreach ( entity spawn in enemyStartSpawns ) - averageEnemySpawns += spawn.GetOrigin() - - averageEnemySpawns /= startSpawns.len() + if( Distance2D( spawnpoint.GetOrigin(), allyFlagSpot ) > Distance2D( spawnpoint.GetOrigin(), enemyFlagSpot ) ) + return false - return Distance2D( spawnpoint.GetOrigin(), averageEnemySpawns ) / Distance2D( averageFriendlySpawns, averageEnemySpawns ) > 0.35 + return true } void function CTFInitPlayer( entity player ) @@ -164,6 +159,9 @@ void function CreateFlags() flag.SetValueForModelKey( CTF_FLAG_MODEL ) SetTeam( flag, flagTeam ) flag.MarkAsNonMovingAttachment() + flag.Minimap_AlwaysShow( TEAM_IMC, null ) // show flag icon on minimap + flag.Minimap_AlwaysShow( TEAM_MILITIA, null ) + flag.Minimap_SetAlignUpright( true ) DispatchSpawn( flag ) flag.SetModel( CTF_FLAG_MODEL ) flag.SetOrigin( spawn.GetOrigin() + < 0, 0, base.GetBoundingMaxs().z * 2 > ) // ensure flag doesn't spawn clipped into geometry @@ -291,7 +289,7 @@ void function GiveFlag( entity player, entity flag ) PlayFactionDialogueToTeamExceptPlayer( "ctf_flagPickupFriendly", player.GetTeam(), player ) MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerHasFriendlyFlag, player, player ) - EmitSoundOnEntityToTeam( flag, "UI_CTF_EnemyGrabFlag", flag.GetTeam() ) + EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyGrabFlag", flag.GetTeam() ) SetFlagStateForTeam( flag.GetTeam(), eFlagState.Away ) // used for held } @@ -339,14 +337,13 @@ void function DropFlag( entity player, bool realDrop = true ) file.imcCaptureAssistList.append( player ) else file.militiaCaptureAssistList.append( player ) - + // do notifications MessageToPlayer( player, eEventNotifications.YouDroppedTheEnemyFlag ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagDrop" ) - + MessageToTeam( player.GetTeam(), eEventNotifications.PlayerDroppedEnemyFlag, player, player ) // todo need a sound here maybe - MessageToTeam( GetOtherTeam( player.GetTeam() ), eEventNotifications.PlayerDroppedFriendlyFlag, player, player ) // todo need a sound here maybe } @@ -417,20 +414,33 @@ void function CaptureFlag( entity player, entity flag ) assistList = file.militiaCaptureAssistList foreach( entity assistPlayer in assistList ) + { if ( player != assistPlayer ) AddPlayerScore( assistPlayer, "FlagCaptureAssist", player ) + if( !HasPlayerCompletedMeritScore( assistPlayer ) ) + { + AddPlayerScore( assistPlayer, "ChallengeCTFCapAssist" ) + SetPlayerChallengeMeritScore( assistPlayer ) + } + } assistList.clear() - + // notifs MessageToPlayer( player, eEventNotifications.YouCapturedTheEnemyFlag ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_PlayerScore" ) + if( !HasPlayerCompletedMeritScore( player ) ) + { + AddPlayerScore( player, "ChallengeCTFRetAssist" ) + SetPlayerChallengeMeritScore( player ) + } + MessageToTeam( team, eEventNotifications.PlayerCapturedEnemyFlag, player, player ) EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamScore", player.GetTeam(), player ) MessageToTeam( GetOtherTeam( team ), eEventNotifications.PlayerCapturedFriendlyFlag, player, player ) - EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScore", flag.GetTeam() ) + EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScores", flag.GetTeam() ) if ( GameRules_GetTeamScore( team ) == GameMode_GetRoundScoreLimit( GAMETYPE ) - 1 ) { @@ -446,6 +456,9 @@ void function OnPlayerEntersFlagReturnTrigger( entity trigger, entity player ) flag = file.imcFlag else flag = file.militiaFlag + + if( !IsValid( flag ) || !IsValid( player ) ) + return if ( !player.IsPlayer() || player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null ) return @@ -481,6 +494,7 @@ void function TryReturnFlag( entity player, entity flag ) }) player.EndSignal( "FlagReturnEnded" ) + flag.EndSignal( "FlagReturnEnded" ) // avoid multiple players to return one flag at once player.EndSignal( "OnDeath" ) wait CTF_GetFlagReturnTime() @@ -488,12 +502,19 @@ void function TryReturnFlag( entity player, entity flag ) // flag return succeeded // return flag ResetFlag( flag ) - + flag.Signal( "FlagReturnEnded" ) + // do notifications for return MessageToPlayer( player, eEventNotifications.YouReturnedFriendlyFlag ) AddPlayerScore( player, "FlagReturn", player ) player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, 1 ) + if( !HasPlayerCompletedMeritScore( player ) ) + { + AddPlayerScore( player, "ChallengeCTFRetAssist" ) + SetPlayerChallengeMeritScore( player ) + } + MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerReturnedFriendlyFlag, null, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_TeamReturnsFlag", flag.GetTeam() ) PlayFactionDialogueToTeam( "ctf_flagReturnedFriendly", flag.GetTeam() ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut index 27eef177..4bff6038 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut @@ -6,6 +6,9 @@ void function FFA_Init() ScoreEvent_SetupEarnMeterValuesForMixedModes() AddCallback_OnPlayerKilled( OnPlayerKilled ) + + // modified for northstar + AddCallback_OnClientConnected( OnClientConnected ) } void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) @@ -16,4 +19,18 @@ void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) // why isn't this PGS_SCORE? odd game attacker.AddToPlayerGameStat( PGS_ASSAULT_SCORE, 1 ) } +} + +// modified for northstar +void function OnClientConnected( entity player ) +{ + thread FFAPlayerScoreThink( player ) // good to have this! instead of DisconnectCallback this could handle a null player +} + +void function FFAPlayerScoreThink( entity player ) +{ + int team = player.GetTeam() + + player.WaitSignal( "OnDestroy" ) // this can handle disconnecting + AddTeamScore( team, -GameRules_GetTeamScore( team ) ) }
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut index 9d8f84b5..6d0fd3c7 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut @@ -17,6 +17,7 @@ void function GamemodeFRA_Init() ScoreEvent_SetEarnMeterValues( "PilotBatteryPickup", 0.0, 0.34 ) EarnMeterMP_SetPassiveMeterGainEnabled( false ) PilotBattery_SetMaxCount( 3 ) + SetupGenericFFAChallenge() AddCallback_OnPlayerKilled( FRARemoveEarnMeter ) } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut index 31c85a57..8999231d 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut @@ -8,6 +8,8 @@ struct { float lastDamageInfoTime bool shouldDoHighlights + + table< entity, int > pilotstreak } file void function GamemodeLts_Init() @@ -34,6 +36,37 @@ void function GamemodeLts_Init() ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() ) ClassicMP_ForceDisableEpilogue( true ) AddCallback_GameStateEnter( eGameState.Playing, WaitForThirtySecondsLeft ) + + AddCallback_OnClientConnected( SetupPlayerLTSChallenges ) //Just to make up the Match Goals tracking + AddCallback_OnClientDisconnected( RemovePlayerLTSChallenges ) //Safety removal of data to prevent crashes + AddCallback_OnPlayerKilled( LTSChallengeForPlayerKilled ) +} + +void function SetupPlayerLTSChallenges( entity player ) +{ + file.pilotstreak[ player ] <- 0 +} + +void function RemovePlayerLTSChallenges( entity player ) +{ + if( player in file.pilotstreak ) + delete file.pilotstreak[ player ] +} + +void function LTSChallengeForPlayerKilled( entity victim, entity attacker, var damageInfo ) +{ + if ( victim == attacker || !attacker.IsPlayer() || GetGameState() != eGameState.Playing ) + return + + if ( victim.IsPlayer() && attacker in file.pilotstreak ) + { + file.pilotstreak[attacker]++ + if( file.pilotstreak[attacker] >= 2 && !HasPlayerCompletedMeritScore( attacker ) ) + { + AddPlayerScore( attacker, "ChallengeLTS" ) + SetPlayerChallengeMeritScore( attacker ) + } + } } void function WaitForThirtySecondsLeft() diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut index 659dbb7a..768bbde1 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut @@ -180,6 +180,12 @@ void function MarkPlayers( entity imcMark, entity militiaMark ) entity livingMark = GetMarked( GetOtherTeam( deadMark.GetTeam() ) ) livingMark.SetPlayerGameStat( PGS_DEFENSE_SCORE, livingMark.GetPlayerGameStat( PGS_DEFENSE_SCORE ) + 1 ) + if( !HasPlayerCompletedMeritScore( livingMark ) ) + { + AddPlayerScore( livingMark, "ChallengeMFD" ) + SetPlayerChallengeMeritScore( livingMark ) + } + // thread this so we don't kill our own thread thread AddTeamScore( livingMark.GetTeam(), 1 ) } @@ -188,10 +194,22 @@ void function UpdateMarksForKill( entity victim, entity attacker, var damageInfo { if ( victim == GetMarked( victim.GetTeam() ) ) { - MessageToAll( eEventNotifications.MarkedForDeathKill, null, victim, attacker.GetEncodedEHandle() ) + // handle suicides. Not sure what the actual message is that vanilla shows for this + // but this will prevent crashing for now + bool isSuicide = IsSuicide( victim, attacker, DamageInfo_GetDamageSourceIdentifier( damageInfo ) ) + entity actualAttacker = isSuicide ? victim : attacker + + MessageToAll( eEventNotifications.MarkedForDeathKill, null, victim, actualAttacker.GetEncodedEHandle() ) svGlobal.levelEnt.Signal( "MarkKilled", { mark = victim } ) - if ( attacker.IsPlayer() ) + if ( !isSuicide && attacker.IsPlayer() ) + { attacker.SetPlayerGameStat( PGS_ASSAULT_SCORE, attacker.GetPlayerGameStat( PGS_ASSAULT_SCORE ) + 1 ) + if( !HasPlayerCompletedMeritScore( attacker ) ) + { + AddPlayerScore( attacker, "ChallengeMFD" ) + SetPlayerChallengeMeritScore( attacker ) + } + } } }
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut index 57355ad8..c91c27d1 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut @@ -19,6 +19,7 @@ void function GamemodePs_Init() AddCallback_OnPlayerKilled( GiveScoreForPlayerKill )
ScoreEvent_SetupEarnMeterValuesForMixedModes()
SetTimeoutWinnerDecisionFunc( CheckScoreForDraw )
+ SetupGenericFFAChallenge()
// spawnzone stuff
SetShouldCreateMinimapSpawnZones( true )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut index cb277b00..4617476e 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut @@ -18,6 +18,7 @@ void function GamemodeSpeedball_Init() Riff_ForceTitanAvailability( eTitanAvailability.Never ) Riff_ForceSetEliminationMode( eEliminationMode.Pilots ) ScoreEvent_SetupEarnMeterValuesForMixedModes() + SetupGenericFFAChallenge() AddSpawnCallbackEditorClass( "script_ref", "info_speedball_flag", CreateFlag ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut index 5c0e6fec..61ede2d4 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut @@ -6,6 +6,7 @@ void function GamemodeTdm_Init() AddCallback_OnPlayerKilled( GiveScoreForPlayerKill )
ScoreEvent_SetupEarnMeterValuesForMixedModes()
SetTimeoutWinnerDecisionFunc( CheckScoreForDraw )
+ SetupGenericTDMChallenge()
}
void function GiveScoreForPlayerKill( entity victim, entity attacker, var damageInfo )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut index 6b30a399..3ba84394 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut @@ -2,6 +2,11 @@ global function GamemodeTTDM_Init const float TTDMIntroLength = 15.0
+struct
+{
+ table< entity, int > challengeCount
+} file
+
void function GamemodeTTDM_Init()
{
Riff_ForceSetSpawnAsTitan( eSpawnAsTitan.Always )
@@ -14,6 +19,8 @@ void function GamemodeTTDM_Init() ClassicMP_ForceDisableEpilogue( true )
SetTimeoutWinnerDecisionFunc( CheckScoreForDraw )
+ AddCallback_OnClientConnected( SetupPlayerTTDMChallenges ) //Just to make up the Match Goals tracking
+ AddCallback_OnClientDisconnected( RemovePlayerTTDMChallenges ) //Safety removal of data to prevent crashes
AddCallback_OnPlayerKilled( AddTeamScoreForPlayerKilled ) // dont have to track autotitan kills since you cant leave your titan in this mode
// probably needs scoreevent earnmeter values
@@ -56,6 +63,17 @@ void function TTDMIntroShowIntermissionCam( entity player ) thread PlayerWatchesTTDMIntroIntermissionCam( player )
}
+void function SetupPlayerTTDMChallenges( entity player )
+{
+ file.challengeCount[ player ] <- 0
+}
+
+void function RemovePlayerTTDMChallenges( entity player )
+{
+ if( player in file.challengeCount )
+ delete file.challengeCount[ player ]
+}
+
void function PlayerWatchesTTDMIntroIntermissionCam( entity player )
{
player.EndSignal( "OnDestroy" )
@@ -79,6 +97,19 @@ void function AddTeamScoreForPlayerKilled( entity victim, entity attacker, var d if ( victim == attacker || !victim.IsPlayer() || !attacker.IsPlayer() && GetGameState() == eGameState.Playing )
return
+ if( victim in file.challengeCount )
+ file.challengeCount[victim] = 0
+
+ if( attacker in file.challengeCount )
+ {
+ file.challengeCount[attacker]++
+ if( file.challengeCount[attacker] >= 2 && !HasPlayerCompletedMeritScore( attacker ) )
+ {
+ AddPlayerScore( attacker, "ChallengeTTDM" )
+ SetPlayerChallengeMeritScore( attacker )
+ }
+ }
+
AddTeamScore( GetOtherTeam( victim.GetTeam() ), 1 )
}
|